use std::{fmt::Display, str::FromStr};
use icu_collator::CaseFirst;
use crate::{
object::{JsObject, ObjectData},
Context, JsNativeError, JsResult, JsString, JsValue,
};
#[derive(Debug, Default)]
pub(super) struct IntlOptions<O> {
pub(super) matcher: LocaleMatcher,
pub(super) service_options: O,
}
pub(super) trait OptionType: Sized {
fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult<Self>;
}
pub(super) trait OptionTypeParsable: FromStr {}
impl<T: OptionTypeParsable> OptionType for T
where
T::Err: Display,
{
fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult<Self> {
value
.to_string(context)?
.to_std_string_escaped()
.parse::<Self>()
.map_err(|err| JsNativeError::range().with_message(err.to_string()).into())
}
}
impl OptionType for bool {
fn from_value(value: JsValue, _: &mut Context<'_>) -> JsResult<Self> {
Ok(value.to_boolean())
}
}
impl OptionType for JsString {
fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult<Self> {
value.to_string(context)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub(super) enum LocaleMatcher {
Lookup,
#[default]
BestFit,
}
#[derive(Debug)]
pub(super) struct ParseLocaleMatcherError;
impl Display for ParseLocaleMatcherError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
"provided string was not `lookup` or `best fit`".fmt(f)
}
}
impl FromStr for LocaleMatcher {
type Err = ParseLocaleMatcherError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"lookup" => Ok(Self::Lookup),
"best fit" => Ok(Self::BestFit),
_ => Err(ParseLocaleMatcherError),
}
}
}
impl OptionTypeParsable for LocaleMatcher {}
impl OptionType for CaseFirst {
fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult<Self> {
match value.to_string(context)?.to_std_string_escaped().as_str() {
"upper" => Ok(Self::UpperFirst),
"lower" => Ok(Self::LowerFirst),
"false" => Ok(Self::Off),
_ => Err(JsNativeError::range()
.with_message("provided string was not `upper`, `lower` or `false`")
.into()),
}
}
}
pub(super) fn get_option<T: OptionType>(
options: &JsObject,
property: &[u16],
required: bool,
context: &mut Context<'_>,
) -> JsResult<Option<T>> {
let value = options.get(property, context)?;
if value.is_undefined() {
return if required {
Err(JsNativeError::range()
.with_message("GetOption: option value cannot be undefined")
.into())
} else {
Ok(None)
};
}
T::from_value(value, context).map(Some)
}
#[allow(unused)]
pub(super) fn get_number_option(
options: &JsObject,
property: &[u16],
minimum: f64,
maximum: f64,
fallback: Option<f64>,
context: &mut Context<'_>,
) -> JsResult<Option<f64>> {
let value = options.get(property, context)?;
default_number_option(&value, minimum, maximum, fallback, context)
}
#[allow(unused)]
pub(super) fn default_number_option(
value: &JsValue,
minimum: f64,
maximum: f64,
fallback: Option<f64>,
context: &mut Context<'_>,
) -> JsResult<Option<f64>> {
if value.is_undefined() {
return Ok(fallback);
}
let value = value.to_number(context)?;
if value.is_nan() || value < minimum || value > maximum {
return Err(JsNativeError::range()
.with_message("DefaultNumberOption: value is out of range.")
.into());
}
Ok(Some(value.floor()))
}
pub(super) fn get_options_object(options: &JsValue) -> JsResult<JsObject> {
match options {
JsValue::Undefined => {
Ok(JsObject::from_proto_and_data(None, ObjectData::ordinary()))
}
JsValue::Object(obj) => {
Ok(obj.clone())
}
_ => Err(JsNativeError::typ()
.with_message("GetOptionsObject: provided options is not an object")
.into()),
}
}
pub(super) fn coerce_options_to_object(
options: &JsValue,
context: &mut Context<'_>,
) -> JsResult<JsObject> {
if options.is_undefined() {
return Ok(JsObject::from_proto_and_data_with_shared_shape(
context.root_shape(),
None,
ObjectData::ordinary(),
));
}
options.to_object(context)
}