diff --git a/README.md b/README.md index 25b23b1..1300e2b 100644 --- a/README.md +++ b/README.md @@ -103,10 +103,10 @@ use decasify::{Locale, StyleGuide, StyleOptions}; fn demo() { let input = "ILIK SU VE İTEN RÜZGARLAR"; - let output = titlecase(input, Locale::TR, StyleGuide::LanguageDefault, StyleOptions::default()); + let output = titlecase(input, Locale::TR, StyleGuide::LanguageDefault, StyleOptions::default()).unwrap(); eprintln! {"{output}"}; let input = "title with a twist: a colon"; - let output = titlecase(input, Locale::EN, StyleGuide::DaringFireball, StyleOptions::default()); + let output = titlecase(input, Locale::EN, StyleGuide::DaringFireball, StyleOptions::default()).unwrap(); eprintln! {"{output}"}; } ``` diff --git a/spec/decasify_spec.lua b/spec/decasify_spec.lua index 3e1a326..13d39ec 100644 --- a/spec/decasify_spec.lua +++ b/spec/decasify_spec.lua @@ -62,6 +62,15 @@ describe("decasify", function () end) end) + it("should balk at unparsable values", function () + assert.error(function () + case("foo", nil, "foo") + end) + assert.error(function () + case("foo", nil, 1) + end) + end) + it("should not balk at passing all options through", function () local text = "foo: a baz" assert.equal("Foo: A Baz", case(text, "title", "en", "gruber")) @@ -89,11 +98,23 @@ describe("decasify", function () assert.no.error(function () titlecase("foo", "tr") end) + assert.no.error(function () + titlecase("foo", "tr") + end) assert.no.error(function () titlecase("foo") end) end) + it("should balk at unparsable values", function () + assert.error(function () + titlecase("foo", "foo") + end) + assert.error(function () + titlecase("foo", 1) + end) + end) + it("should cooperate with English style guides", function () local text = "foo: a baz" local cmos = "Foo: a Baz" @@ -132,6 +153,15 @@ describe("decasify", function () end) end) + it("should balk at unparsable values", function () + assert.error(function () + lowercase("foo", "foo") + end) + assert.error(function () + lowercase("foo", 1) + end) + end) + it("should default to handling string as English", function () local result = lowercase("IBUPROFIN") assert.equal("ibuprofin", result) @@ -153,6 +183,15 @@ describe("decasify", function () end) end) + it("should balk at unparsable values", function () + assert.error(function () + uppercase("foo", "foo") + end) + assert.error(function () + uppercase("foo", 1) + end) + end) + it("should default to handling string as English", function () local result = uppercase("ibuprofin") assert.equal("IBUPROFIN", result) @@ -174,6 +213,15 @@ describe("decasify", function () end) end) + it("should balk at unparsable values", function () + assert.error(function () + sentencecase("foo", "foo") + end) + assert.error(function () + sentencecase("foo", 1) + end) + end) + it("should default to handling string as English", function () local result = sentencecase("insert BIKE here") assert.equal("Insert bike here", result) diff --git a/src/bin/decasify.rs b/src/bin/decasify.rs index 8a7e8af..05fa576 100644 --- a/src/bin/decasify.rs +++ b/src/bin/decasify.rs @@ -1,53 +1,30 @@ // SPDX-FileCopyrightText: © 2023 Caleb Maclennan // SPDX-License-Identifier: LGPL-3.0-only -use decasify::cli::Cli; -use decasify::{lowercase, sentencecase, titlecase, uppercase}; -use decasify::{Case, Locale, StyleGuide, StyleOptions, StyleOptionsBuilder}; - -use snafu::prelude::*; - use clap::CommandFactory; use std::io; use std::io::BufRead; -#[derive(Snafu)] -enum Error { - #[snafu(display("Failed to identify input"))] - Input {}, - - #[snafu(display("Failed to resolve a known locale"))] - Locale {}, - - #[snafu(display("Failed to resolve a known case"))] - Case {}, - - #[snafu(display("Failed to resolve a known style guide"))] - StyleGuide {}, -} - -// Clap CLI errors are reported using the Debug trait, but Snafu sets up the Display trait. -// So we delegate. c.f. https://github.com/shepmaster/snafu/issues/110 -impl std::fmt::Debug for Error { - fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { - std::fmt::Display::fmt(self, fmt) - } -} - -type Result = std::result::Result; +use decasify::cli::Cli; +use decasify::types::Result; +use decasify::{lowercase, sentencecase, titlecase, uppercase}; +use decasify::{Case, Locale, StyleGuide, StyleOptions, StyleOptionsBuilder}; fn main() -> Result<()> { let version = option_env!("VERGEN_GIT_DESCRIBE").unwrap_or_else(|| env!("CARGO_PKG_VERSION")); let app = Cli::command().version(version); let matches = app.get_matches(); - let locale = matches.get_one::("locale").context(LocaleSnafu)?; + let locale = matches + .get_one::("locale") + .unwrap_or(&Locale::default()) + .to_owned(); let case = matches .get_one::("case") - .context(CaseSnafu)? + .unwrap_or(&Case::default()) .to_owned(); let style = matches .get_one::("style") - .context(StyleGuideSnafu)? + .unwrap_or(&StyleGuide::default()) .to_owned(); let opts = if let Some(overrides) = matches.get_many::("overrides") { StyleOptionsBuilder::new() @@ -60,27 +37,23 @@ fn main() -> Result<()> { true => { let input: Vec = matches .get_many::("input") - .context(InputSnafu)? + .unwrap() .cloned() .collect(); let input: Vec = vec![input.join(" ")]; process( input.iter().map(|ln| ln.to_string()), - *locale, + locale, case, style, opts, - ); + ) + } + false => { + let stdin = io::stdin().lock().lines().map(|ln| ln.unwrap()); + process(stdin, locale, case, style, opts) } - false => process( - io::stdin().lock().lines().map(|ln| ln.unwrap()), - *locale, - case, - style, - opts, - ), } - Ok(()) } fn process>( @@ -89,15 +62,16 @@ fn process>( case: Case, style: StyleGuide, opts: StyleOptions, -) { +) -> Result<()> { for string in strings { let output = match case { - Case::Title => titlecase(string, locale, style.clone(), opts.clone()), - Case::Lower => lowercase(string, locale), - Case::Upper => uppercase(string, locale), - Case::Sentence => sentencecase(string, locale), + Case::Title => titlecase(string, locale, style.clone(), opts.clone())?, + Case::Lower => lowercase(string, locale)?, + Case::Upper => uppercase(string, locale)?, + Case::Sentence => sentencecase(string, locale)?, _ => unreachable!(), }; - println!("{output}") + println!("{output}"); } + Ok(()) } diff --git a/src/content.rs b/src/content.rs index 394f0d5..0f63f61 100644 --- a/src/content.rs +++ b/src/content.rs @@ -1,13 +1,11 @@ // SPDX-FileCopyrightText: © 2023 Caleb Maclennan // SPDX-License-Identifier: LGPL-3.0-only -pub use crate::types::Word; - use regex::Regex; use std::{borrow::Cow, fmt, fmt::Display, str::FromStr}; use unicode_titlecase::StrTitleCase; -use snafu::prelude::*; +use crate::types::{Error, Result, Word}; #[derive(Clone, Debug)] #[non_exhaustive] @@ -22,22 +20,6 @@ pub enum Segment { Word(Word), } -#[derive(Snafu)] -pub enum Error { - #[snafu(display("Unable to cast str to Chunk"))] - StrToChunk {}, -} - -// Clap CLI errors are reported using the Debug trait, but Snafu sets up the Display trait. -// So we delegate. c.f. https://github.com/shepmaster/snafu/issues/110 -impl std::fmt::Debug for Error { - fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { - std::fmt::Display::fmt(self, fmt) - } -} - -pub type Result = std::result::Result; - fn split_chunk(s: &str) -> Chunk { let mut segments: Vec = Vec::new(); let captures = Regex::new(r"(?\p{Whitespace}+)|(?\P{Whitespace}+)").unwrap(); diff --git a/src/lib.rs b/src/lib.rs index b84e470..268ad73 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,12 +6,13 @@ mod content; mod traits; -mod types; +pub mod types; pub use content::Chunk; #[cfg(feature = "unstable-trait")] pub use traits::Decasify; pub use types::{Case, Locale, StyleGuide, StyleOptions, StyleOptionsBuilder, Word}; +pub use types::{Error, Result}; #[cfg(feature = "cli")] #[doc(hidden)] @@ -33,18 +34,28 @@ mod en; mod tr; /// Convert a string to a specific case following typesetting conventions for a target locale -pub fn case( +pub fn case( chunk: impl Into, - case: impl Into, - locale: impl Into, - style: impl Into, - opts: impl Into, -) -> String { + case: TC, + locale: TL, + style: TS, + opts: TO, +) -> Result +where + TC: TryInto, + TL: TryInto, + TS: TryInto, + TO: TryInto, + Error: From, + Error: From, + Error: From, + Error: From, +{ let chunk: Chunk = chunk.into(); - let case: Case = case.into(); - let locale: Locale = locale.into(); - let style: StyleGuide = style.into(); - let opts: StyleOptions = opts.into(); + let case: Case = case.try_into()?; + let locale: Locale = locale.try_into()?; + let style: StyleGuide = style.try_into()?; + let opts: StyleOptions = opts.try_into()?; match case { Case::Lower => lowercase(chunk, locale), Case::Upper => uppercase(chunk, locale), @@ -54,50 +65,70 @@ pub fn case( } /// Convert a string to title case following typesetting conventions for a target locale -pub fn titlecase( +pub fn titlecase( chunk: impl Into, - locale: impl Into, - style: impl Into, - opts: impl Into, -) -> String { + locale: TL, + style: TS, + opts: TO, +) -> Result +where + TL: TryInto, + TS: TryInto, + TO: TryInto, + Error: From, + Error: From, + Error: From, +{ let chunk: Chunk = chunk.into(); - let locale: Locale = locale.into(); - let style: StyleGuide = style.into(); - let opts: StyleOptions = opts.into(); - match locale { + let locale: Locale = locale.try_into()?; + let style: StyleGuide = style.try_into()?; + let opts: StyleOptions = opts.try_into()?; + Ok(match locale { Locale::EN => en::titlecase(chunk, style, opts), Locale::TR => tr::titlecase(chunk, style, opts), - } + }) } /// Convert a string to lower case following typesetting conventions for a target locale -pub fn lowercase(chunk: impl Into, locale: impl Into) -> String { +pub fn lowercase(chunk: impl Into, locale: TL) -> Result +where + TL: TryInto, + Error: From, +{ let chunk: Chunk = chunk.into(); - let locale: Locale = locale.into(); - match locale { + let locale: Locale = locale.try_into()?; + Ok(match locale { Locale::EN => en::lowercase(chunk), Locale::TR => tr::lowercase(chunk), - } + }) } /// Convert a string to upper case following typesetting conventions for a target locale -pub fn uppercase(chunk: impl Into, locale: impl Into) -> String { +pub fn uppercase(chunk: impl Into, locale: TL) -> Result +where + TL: TryInto, + Error: From, +{ let chunk: Chunk = chunk.into(); - let locale: Locale = locale.into(); - match locale { + let locale: Locale = locale.try_into()?; + Ok(match locale { Locale::EN => en::uppercase(chunk), Locale::TR => tr::uppercase(chunk), - } + }) } /// Convert a string to sentence case following typesetting conventions for a target locale -pub fn sentencecase(chunk: impl Into, locale: impl Into) -> String { +pub fn sentencecase(chunk: impl Into, locale: TL) -> Result +where + TL: TryInto, + Error: From, +{ let chunk: Chunk = chunk.into(); - let locale: Locale = locale.into(); - match locale { + let locale: Locale = locale.try_into()?; + Ok(match locale { Locale::EN => en::sentencecase(chunk), Locale::TR => tr::sentencecase(chunk), - } + }) } fn get_override(word: &Word, overrides: &Option>, case_fn: F) -> Option diff --git a/src/lua.rs b/src/lua.rs index 6170487..b1ba797 100644 --- a/src/lua.rs +++ b/src/lua.rs @@ -4,33 +4,95 @@ use crate::*; use mlua::prelude::*; +use crate::types::{Error, Result}; + +macro_rules! impl_into_luaresult { + ($($t:ty),*) => { + $( + impl Into> for $t { + fn into(self) -> LuaResult<$t> { + Ok(self) + } + } + )* + }; +} + +impl_into_luaresult!(Locale, Case, StyleGuide, StyleOptions); + +impl From for LuaError { + fn from(err: Error) -> LuaError { + LuaError::RuntimeError(err.to_string()) + } +} + +impl TryFrom for Locale { + type Error = Error; + fn try_from(s: LuaString) -> Result { + s.to_string_lossy().try_into() + } +} + +impl TryFrom for Case { + type Error = Error; + fn try_from(s: LuaString) -> Result { + s.to_string_lossy().try_into() + } +} + +impl TryFrom for StyleGuide { + type Error = Error; + fn try_from(s: LuaString) -> Result { + s.to_string_lossy().try_into() + } +} + #[mlua::lua_module] fn decasify(lua: &Lua) -> LuaResult { let exports = lua.create_table()?; exports.set( "case", - LuaFunction::wrap_raw::<_, (Chunk, Case, Locale, StyleGuide, StyleOptions)>(case), + lua.create_function( + |_, + (chunk, case_, locale, styleguide, styleoptions): ( + Chunk, + Case, + Locale, + StyleGuide, + StyleOptions, + )| { Ok(case(chunk, case_, locale, styleguide, styleoptions)?) }, + )?, )?; exports.set( "titlecase", - LuaFunction::wrap_raw::<_, (Chunk, Locale, StyleGuide, StyleOptions)>(titlecase), + lua.create_function( + |_, + (chunk, locale, styleguide, styleoptions): ( + Chunk, + Locale, + StyleGuide, + StyleOptions, + )| { Ok(titlecase(chunk, locale, styleguide, styleoptions)?) }, + )?, )?; exports.set( "lowercase", - LuaFunction::wrap_raw::<_, (Chunk, Locale)>(lowercase), + lua.create_function(|_, (chunk, locale): (Chunk, Locale)| Ok(lowercase(chunk, locale)?))?, )?; exports.set( "uppercase", - LuaFunction::wrap_raw::<_, (Chunk, Locale)>(uppercase), + lua.create_function(|_, (chunk, locale): (Chunk, Locale)| Ok(uppercase(chunk, locale)?))?, )?; exports.set( "sentencecase", - LuaFunction::wrap_raw::<_, (Chunk, Locale)>(sentencecase), + lua.create_function(|_, (chunk, locale): (Chunk, Locale)| { + Ok(sentencecase(chunk, locale)?) + })?, )?; let mt = lua.create_table()?; let decasify = lua.create_function( move |_, - (_, chunk, case_, locale, styleguide, opts): ( + (_, chunk, case_, locale, styleguide, styleoptions): ( LuaTable, Chunk, Case, @@ -43,8 +105,8 @@ fn decasify(lua: &Lua) -> LuaResult { case_, locale, styleguide, - opts.unwrap_or_default(), - )) + styleoptions.unwrap_or_default(), + )?) }, )?; mt.set("__call", decasify)?; @@ -58,10 +120,12 @@ fn decasify(lua: &Lua) -> LuaResult { #[cfg_attr(docsrs, doc(cfg(feature = "luamodule")))] impl FromLua for Chunk { fn from_lua(value: LuaValue, _: &Lua) -> LuaResult { - match value { - LuaValue::String(s) => Ok(s.to_string_lossy().into()), - _ => Ok("".into()), + let chunk = match value { + LuaValue::String(s) => s.to_string_lossy(), + _ => String::from(""), } + .into(); + Ok(chunk) } } @@ -69,10 +133,11 @@ impl FromLua for Chunk { impl FromLua for Locale { fn from_lua(value: LuaValue, _: &Lua) -> LuaResult { match value { - LuaValue::String(s) => Ok(s.to_string_lossy().into()), - LuaValue::Nil => Ok(Self::default()), - _ => unimplemented!(), + LuaValue::String(s) => s.try_into()?, + LuaValue::Nil => Self::default(), + _ => value.to_string().unwrap_or_default().try_into()?, } + .into() } } @@ -80,10 +145,11 @@ impl FromLua for Locale { impl FromLua for Case { fn from_lua(value: LuaValue, _: &Lua) -> LuaResult { match value { - LuaValue::String(s) => Ok(s.to_string_lossy().into()), - LuaValue::Nil => Ok(Self::default()), - _ => unimplemented!(), + LuaValue::String(s) => s.try_into()?, + LuaValue::Nil => Self::default(), + _ => value.to_string().unwrap_or_default().try_into()?, } + .into() } } @@ -91,10 +157,11 @@ impl FromLua for Case { impl FromLua for StyleGuide { fn from_lua(value: LuaValue, _: &Lua) -> LuaResult { match value { - LuaValue::String(s) => Ok(s.to_string_lossy().into()), - LuaValue::Nil => Ok(Self::default()), - _ => unimplemented!(), + LuaValue::String(s) => s.try_into()?, + LuaValue::Nil => Self::default(), + _ => value.to_string().unwrap_or_default().try_into()?, } + .into() } } @@ -113,10 +180,11 @@ impl FromLua for StyleOptions { .collect(); builder = builder.overrides(overrides); } - Ok(builder.build()) + builder.build() } - LuaValue::Nil => Ok(Self::default()), - _ => unimplemented!(), + LuaValue::Nil => Self::default(), + _ => value.to_string().unwrap_or_default().try_into()?, } + .into() } } diff --git a/src/python.rs b/src/python.rs index 00a9a16..3e8d063 100644 --- a/src/python.rs +++ b/src/python.rs @@ -4,6 +4,12 @@ use crate::types::*; use pyo3::prelude::*; +impl From for PyErr { + fn from(err: crate::types::Error) -> Self { + pyo3::exceptions::PyValueError::new_err(err.to_string()) + } +} + #[pymodule] fn decasify(module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; @@ -33,7 +39,7 @@ fn case( Some(words) => StyleOptionsBuilder::new().overrides(words).build(), None => StyleOptions::default(), }; - Ok(crate::case(&input, case, locale, style, opts)) + Ok(crate::case(&input, case, locale, style, opts)?) } #[pyfunction] @@ -48,23 +54,23 @@ fn titlecase( Some(words) => StyleOptionsBuilder::new().overrides(words).build(), None => StyleOptions::default(), }; - Ok(crate::titlecase(&input, locale, style, opts)) + Ok(crate::titlecase(&input, locale, style, opts)?) } #[pyfunction] #[pyo3(signature = (input, locale))] fn lowercase(input: String, locale: Locale) -> PyResult { - Ok(crate::lowercase(&input, locale)) + Ok(crate::lowercase(&input, locale)?) } #[pyfunction] #[pyo3(signature = (input, locale))] fn uppercase(input: String, locale: Locale) -> PyResult { - Ok(crate::uppercase(&input, locale)) + Ok(crate::uppercase(&input, locale)?) } #[pyfunction] #[pyo3(signature = (input, locale))] fn sentencecase(input: String, locale: Locale) -> PyResult { - Ok(crate::sentencecase(&input, locale)) + Ok(crate::sentencecase(&input, locale)?) } diff --git a/src/types.rs b/src/types.rs index bf5098f..309bfd0 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,11 +1,12 @@ // SPDX-FileCopyrightText: © 2023 Caleb Maclennan // SPDX-License-Identifier: LGPL-3.0-only +use snafu::prelude::*; +use std::convert::{Infallible, TryFrom}; +use std::fmt::{Debug, Display, Formatter}; use std::str::FromStr; use strum_macros::{Display, VariantNames}; -use snafu::prelude::*; - #[cfg(feature = "pythonmodule")] use pyo3::prelude::*; @@ -13,25 +14,32 @@ use pyo3::prelude::*; use wasm_bindgen::prelude::*; #[derive(Snafu)] +#[snafu(visibility(pub))] pub enum Error { - #[snafu(display("Invalid input language {}", input))] + #[snafu(display("Invalid input language '{input}'"))] Locale { input: String }, - #[snafu(display("Invalid target case {}", input))] + #[snafu(display("Invalid target case '{input}'"))] Case { input: String }, - #[snafu(display("Invalid preferred style guide {}", input))] + #[snafu(display("Invalid preferred style guide '{input}'"))] StyleGuide { input: String }, - #[snafu(display("Invalid style options {}", input))] + #[snafu(display("Invalid style options '{input}'"))] StyleOptions { input: String }, } // Clap CLI errors are reported using the Debug trait, but Snafu sets up the Display trait. // So we delegate. c.f. https://github.com/shepmaster/snafu/issues/110 -impl std::fmt::Debug for Error { - fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { - std::fmt::Display::fmt(self, fmt) +impl Debug for Error { + fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result { + Display::fmt(self, fmt) + } +} + +impl From for Error { + fn from(_: Infallible) -> Self { + unreachable!() } } @@ -99,12 +107,6 @@ pub struct StyleOptions { pub overrides: Option>, } -impl From<&str> for StyleOptions { - fn from(s: &str) -> Self { - Self::from_str(s).unwrap() - } -} - impl FromStr for StyleOptions { type Err = Error; fn from_str(s: &str) -> Result { @@ -115,6 +117,35 @@ impl FromStr for StyleOptions { } } +impl TryFrom<&str> for StyleOptions { + type Error = Error; + fn try_from(s: &str) -> Result { + Self::from_str(s) + } +} + +impl TryFrom for StyleOptions { + type Error = Error; + fn try_from(s: String) -> Result { + Self::from_str(&s) + } +} + +impl TryFrom<&String> for StyleOptions { + type Error = Error; + fn try_from(s: &String) -> Result { + Self::from_str(s) + } +} + +impl TryFrom<&[u8]> for StyleOptions { + type Error = Error; + fn try_from(s: &[u8]) -> Result { + let s = String::from_utf8_lossy(s); + Self::from_str(&s) + } +} + #[derive(Debug)] pub struct StyleOptionsBuilder { overrides: Option>, @@ -155,28 +186,33 @@ impl FromStr for Locale { } } -impl From<&str> for Locale { - fn from(s: &str) -> Self { - Self::from_str(s).unwrap() +impl TryFrom<&str> for Locale { + type Error = Error; + fn try_from(s: &str) -> Result { + Self::from_str(s) } } -impl From for Locale { - fn from(s: String) -> Self { - Self::from_str(s.as_ref()).unwrap() +impl TryFrom for Locale { + type Error = Error; + fn try_from(s: String) -> Result { + Self::from_str(&s) } } -impl From<&String> for Locale { - fn from(s: &String) -> Self { - Self::from_str(s.as_ref()).unwrap() +impl TryFrom<&String> for Locale { + type Error = Error; + fn try_from(s: &String) -> Result { + Self::from_str(s) } } -impl From<&[u8]> for Locale { - fn from(s: &[u8]) -> Self { - let s = String::from_utf8(s.to_vec()).unwrap(); - Self::from_str(s.as_ref()).unwrap() +impl TryFrom<&[u8]> for Locale { + type Error = Error; + + fn try_from(s: &[u8]) -> Result { + let s = String::from_utf8_lossy(s); + Self::from_str(&s) } } @@ -193,28 +229,33 @@ impl FromStr for Case { } } -impl From<&str> for Case { - fn from(s: &str) -> Self { - Self::from_str(s).unwrap() +impl TryFrom<&str> for Case { + type Error = Error; + fn try_from(s: &str) -> Result { + Self::from_str(s) } } -impl From for Case { - fn from(s: String) -> Self { - Self::from_str(s.as_ref()).unwrap() +impl TryFrom for Case { + type Error = Error; + fn try_from(s: String) -> Result { + Self::from_str(&s) } } -impl From<&String> for Case { - fn from(s: &String) -> Self { - Self::from_str(s.as_ref()).unwrap() +impl TryFrom<&String> for Case { + type Error = Error; + fn try_from(s: &String) -> Result { + Self::from_str(s) } } -impl From<&[u8]> for Case { - fn from(s: &[u8]) -> Self { - let s = String::from_utf8(s.to_vec()).unwrap(); - Self::from_str(s.as_ref()).unwrap() +impl TryFrom<&[u8]> for Case { + type Error = Error; + + fn try_from(s: &[u8]) -> Result { + let s = String::from_utf8_lossy(s); + Self::from_str(&s) } } @@ -234,28 +275,24 @@ impl FromStr for StyleGuide { } } -impl From<&str> for StyleGuide { - fn from(s: &str) -> Self { - Self::from_str(s).unwrap() +impl TryFrom<&str> for StyleGuide { + type Error = Error; + fn try_from(s: &str) -> Result { + Self::from_str(s) } } -impl From for StyleGuide { - fn from(s: String) -> Self { - Self::from_str(s.as_ref()).unwrap() +impl TryFrom for StyleGuide { + type Error = Error; + fn try_from(s: String) -> Result { + Self::from_str(&s) } } -impl From<&String> for StyleGuide { - fn from(s: &String) -> Self { - Self::from_str(s.as_ref()).unwrap() - } -} - -impl From<&[u8]> for StyleGuide { - fn from(s: &[u8]) -> Self { - let s = String::from_utf8(s.to_vec()).unwrap(); - Self::from_str(s.as_ref()).unwrap() +impl TryFrom<&String> for StyleGuide { + type Error = Error; + fn try_from(s: &String) -> Result { + Self::from_str(s) } } @@ -267,3 +304,12 @@ impl From> for StyleGuide { } } } + +impl TryFrom<&[u8]> for StyleGuide { + type Error = Error; + + fn try_from(s: &[u8]) -> Result { + let s = String::from_utf8_lossy(s); + Self::from_str(&s) + } +} diff --git a/src/wasm.rs b/src/wasm.rs index a1f363e..be1c5b8 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -15,7 +15,7 @@ pub fn case( opts: Option, ) -> Result { let opts = opts.unwrap_or_default(); - Ok(crate::case(input, case, locale, style, opts)) + Ok(crate::case(input, case, locale, style, opts)?) } #[wasm_bindgen] @@ -26,20 +26,20 @@ pub fn titlecase( opts: Option, ) -> Result { let opts = opts.unwrap_or_default(); - Ok(crate::titlecase(input, locale, style, opts)) + Ok(crate::titlecase(input, locale, style, opts)?) } #[wasm_bindgen] pub fn lowercase(input: &str, locale: Locale) -> Result { - Ok(crate::lowercase(input, locale)) + Ok(crate::lowercase(input, locale)?) } #[wasm_bindgen] pub fn uppercase(input: &str, locale: Locale) -> Result { - Ok(crate::uppercase(input, locale)) + Ok(crate::uppercase(input, locale)?) } #[wasm_bindgen] pub fn sentencecase(input: &str, locale: Locale) -> Result { - Ok(crate::sentencecase(input, locale)) + Ok(crate::sentencecase(input, locale)?) } diff --git a/tests/lib.rs b/tests/lib.rs index 23711e3..25e5dde 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -5,11 +5,11 @@ use decasify::*; #[test] fn cast_from_str() { - let res = titlecase("FIST", "en", "gruber", "default"); + let res = titlecase("FIST", "en", "gruber", "default").unwrap(); assert_eq!(res, "Fist"); - let res = titlecase("FIST", "tr", "", "default"); + let res = titlecase("FIST", "tr", "", "default").unwrap(); assert_eq!(res, "Fıst"); - let res = titlecase("FIST", "tr", "default", "default"); + let res = titlecase("FIST", "tr", "default", "default").unwrap(); assert_eq!(res, "Fıst"); } @@ -20,23 +20,24 @@ fn cast_from_legacy_option() { "en", Some(StyleGuide::DaringFireball), StyleOptions::default(), - ); + ) + .unwrap(); assert_eq!(res, "Fist"); - let res = titlecase("FIST", "en", None, StyleOptions::default()); + let res = titlecase("FIST", "en", None, StyleOptions::default()).unwrap(); assert_eq!(res, "Fist"); } #[test] fn custom_style_guide() { let options: StyleOptions = StyleOptionsBuilder::new().overrides(vec!["fOO"]).build(); - let res = titlecase("foo bar", "tr", StyleGuide::LanguageDefault, options); + let res = titlecase("foo bar", "tr", StyleGuide::LanguageDefault, options).unwrap(); assert_eq!(res, "fOO Bar"); } #[cfg(feature = "unstable-trait")] #[test] fn trait_chery() { - use decasify::Decasify; + use Decasify::Decasify; let s = "WHY THE LONG FACE?"; assert_eq!(s.to_case("sentence", "en", None), "Why the long face?"); assert_eq!( @@ -53,7 +54,7 @@ macro_rules! case { ($name:ident, $case:expr, $locale:expr, $style:expr, $opts:expr, $input:expr, $expected:expr) => { #[test] fn $name() { - let actual = case($input, $case, $locale, $style, $opts); + let actual = case($input, $case, $locale, $style, $opts).unwrap(); assert_eq!(actual, $expected); } }; @@ -103,7 +104,7 @@ macro_rules! titlecase { ($name:ident, $locale:expr, $style:expr, $opts:expr, $input:expr, $expected:expr) => { #[test] fn $name() { - let actual = titlecase($input, $locale, $style, $opts); + let actual = titlecase($input, $locale, $style, $opts).unwrap(); assert_eq!(actual, $expected); } }; @@ -247,7 +248,7 @@ macro_rules! lowercase { ($name:ident, $locale:expr, $input:expr, $expected:expr) => { #[test] fn $name() { - let actual = lowercase($input, $locale); + let actual = lowercase($input, $locale).unwrap(); assert_eq!(actual, $expected); } }; @@ -266,7 +267,7 @@ macro_rules! uppercase { ($name:ident, $locale:expr, $input:expr, $expected:expr) => { #[test] fn $name() { - let actual = uppercase($input, $locale); + let actual = uppercase($input, $locale).unwrap(); assert_eq!(actual, $expected); } }; @@ -285,7 +286,7 @@ macro_rules! sentencecase { ($name:ident, $locale:expr, $input:expr, $expected:expr) => { #[test] fn $name() { - let actual = sentencecase($input, $locale); + let actual = sentencecase($input, $locale).unwrap(); assert_eq!(actual, $expected); } }; diff --git a/typst/src/lib.rs b/typst/src/lib.rs index f867a02..d9b676c 100644 --- a/typst/src/lib.rs +++ b/typst/src/lib.rs @@ -1,12 +1,15 @@ // SPDX-FileCopyrightText: © 2024 Caleb Maclennan // SPDX-License-Identifier: LGPL-3.0-only -use anyhow::Result; -use decasify::{Case, Locale, StyleGuide, StyleOptions, StyleOptionsBuilder}; +use anyhow::{Error, Result}; use wasm_minimal_protocol::{initiate_protocol, wasm_func}; +use decasify::{Case, Locale, StyleGuide, StyleOptions, StyleOptionsBuilder}; + initiate_protocol!(); +pub type TypstResult = Result, Error>; + #[wasm_func] pub fn decasify( data: &[u8], @@ -14,11 +17,11 @@ pub fn decasify( lang: &[u8], style: &[u8], overrides: &[u8], -) -> Result> { +) -> TypstResult { let chunk = String::from_utf8(data.to_vec())?; - let case = Case::from(case); - let locale = Locale::from(lang); - let style = StyleGuide::from(style); + let case = Case::try_from(case)?; + let locale = Locale::try_from(lang)?; + let style = StyleGuide::try_from(style)?; let overrides_str = String::from_utf8(overrides.to_vec())?; let opts = if overrides_str.is_empty() { StyleOptions::default() @@ -26,14 +29,14 @@ pub fn decasify( let overrides = overrides_str.split(',').map(String::from).collect(); StyleOptionsBuilder::new().overrides(overrides).build() }; - Ok(decasify::case(&chunk, case, locale, style, opts).into_bytes()) + Ok(decasify::case(&chunk, case, locale, style, opts)?.into_bytes()) } #[wasm_func] -pub fn titlecase(data: &[u8], lang: &[u8], style: &[u8], overrides: &[u8]) -> Result> { +pub fn titlecase(data: &[u8], lang: &[u8], style: &[u8], overrides: &[u8]) -> TypstResult { let chunk = String::from_utf8(data.to_vec())?; - let locale = Locale::from(lang); - let style = StyleGuide::from(style); + let locale = Locale::try_from(lang)?; + let style = StyleGuide::try_from(style)?; let overrides_str = String::from_utf8(overrides.to_vec())?; let opts = if overrides_str.is_empty() { StyleOptions::default() @@ -41,26 +44,26 @@ pub fn titlecase(data: &[u8], lang: &[u8], style: &[u8], overrides: &[u8]) -> Re let overrides = overrides_str.split(',').map(String::from).collect(); StyleOptionsBuilder::new().overrides(overrides).build() }; - Ok(decasify::titlecase(&chunk, locale, style, opts).into_bytes()) + Ok(decasify::titlecase(&chunk, locale, style, opts)?.into_bytes()) } #[wasm_func] -pub fn lowercase(data: &[u8], lang: &[u8]) -> Result> { +pub fn lowercase(data: &[u8], lang: &[u8]) -> TypstResult { let chunk = String::from_utf8(data.to_vec())?; - let locale = Locale::from(lang); - Ok(decasify::lowercase(&chunk, locale).into_bytes()) + let locale = Locale::try_from(lang)?; + Ok(decasify::lowercase(&chunk, locale)?.into_bytes()) } #[wasm_func] -pub fn uppercase(data: &[u8], lang: &[u8]) -> Result> { +pub fn uppercase(data: &[u8], lang: &[u8]) -> TypstResult { let chunk = String::from_utf8(data.to_vec())?; - let locale = Locale::from(lang); - Ok(decasify::uppercase(&chunk, locale).into_bytes()) + let locale = Locale::try_from(lang)?; + Ok(decasify::uppercase(&chunk, locale)?.into_bytes()) } #[wasm_func] -pub fn sentencecase(data: &[u8], lang: &[u8]) -> Result> { +pub fn sentencecase(data: &[u8], lang: &[u8]) -> TypstResult { let chunk = String::from_utf8(data.to_vec())?; - let locale = Locale::from(lang); - Ok(decasify::sentencecase(&chunk, locale).into_bytes()) + let locale = Locale::try_from(lang)?; + Ok(decasify::sentencecase(&chunk, locale)?.into_bytes()) }