summaryrefslogtreecommitdiff
path: root/src/model/cast.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/model/cast.rs')
-rw-r--r--src/model/cast.rs513
1 files changed, 261 insertions, 252 deletions
diff --git a/src/model/cast.rs b/src/model/cast.rs
index bfde1bdd..833b9e9e 100644
--- a/src/model/cast.rs
+++ b/src/model/cast.rs
@@ -1,15 +1,19 @@
use std::num::NonZeroUsize;
+use std::ops::Add;
use std::str::FromStr;
-use super::{Content, Regex, Selector, Transform, Value};
-use crate::diag::{with_alternative, StrResult};
+use super::{
+ castable, Array, Content, Dict, Func, Label, Regex, Selector, Str, Transform, Value,
+};
+use crate::diag::StrResult;
use crate::doc::{Destination, Lang, Location, Region};
use crate::font::{FontStretch, FontStyle, FontWeight};
use crate::geom::{
- Axes, Corners, Dir, GenAlign, Get, Length, Paint, PartialStroke, Point, Rel, Sides,
+ Axes, Color, Corners, Dir, GenAlign, Get, Length, Paint, PartialStroke, Point, Ratio,
+ Rel, Sides, Smart,
};
use crate::syntax::Spanned;
-use crate::util::{format_eco, EcoString};
+use crate::util::EcoString;
/// Cast from a value to a specific type.
pub trait Cast<V = Value>: Sized {
@@ -18,95 +22,98 @@ pub trait Cast<V = Value>: Sized {
/// Try to cast the value into an instance of `Self`.
fn cast(value: V) -> StrResult<Self>;
+
+ /// Describe the acceptable values.
+ fn describe() -> CastInfo;
+
+ /// Produce an error for an inacceptable value.
+ fn error(value: Value) -> StrResult<Self> {
+ Err(Self::describe().error(&value))
+ }
}
-/// Implement traits for dynamic types.
-#[macro_export]
-#[doc(hidden)]
-macro_rules! __dynamic {
- ($type:ty: $name:literal, $($tts:tt)*) => {
- impl $crate::model::Type for $type {
- const TYPE_NAME: &'static str = $name;
+/// Describes a possible value for a cast.
+#[derive(Debug, Clone)]
+pub enum CastInfo {
+ /// Any value is okay.
+ Any,
+ /// A specific value, plus short documentation for that value.
+ Value(Value, &'static str),
+ /// Any value of a type.
+ Type(&'static str),
+ /// Multiple alternatives.
+ Union(Vec<Self>),
+}
+
+impl CastInfo {
+ /// Produce an error message describing what was expected and what was
+ /// found.
+ pub fn error(&self, found: &Value) -> EcoString {
+ fn accumulate(
+ info: &CastInfo,
+ found: &Value,
+ parts: &mut Vec<EcoString>,
+ matching_type: &mut bool,
+ ) {
+ match info {
+ CastInfo::Any => parts.push("anything".into()),
+ CastInfo::Value(value, _) => {
+ parts.push(value.repr().into());
+ if value.type_name() == found.type_name() {
+ *matching_type = true;
+ }
+ }
+ CastInfo::Type(ty) => parts.push((*ty).into()),
+ CastInfo::Union(options) => {
+ for option in options {
+ accumulate(option, found, parts, matching_type);
+ }
+ }
+ }
}
- castable! {
- $type,
- Expected: <Self as $crate::model::Type>::TYPE_NAME,
- $($tts)*
- @this: Self => this.clone(),
+ let mut matching_type = false;
+ let mut parts = vec![];
+ accumulate(self, found, &mut parts, &mut matching_type);
+
+ let mut msg = String::from("expected ");
+ if parts.is_empty() {
+ msg.push_str(" nothing");
}
- impl From<$type> for $crate::model::Value {
- fn from(v: $type) -> Self {
- $crate::model::Value::Dyn($crate::model::Dynamic::new(v))
- }
+ crate::diag::comma_list(&mut msg, &parts, "or");
+
+ if !matching_type {
+ msg.push_str(", found ");
+ msg.push_str(found.type_name());
}
- };
+
+ msg.into()
+ }
}
-#[doc(inline)]
-pub use crate::__dynamic as dynamic;
+impl Add for CastInfo {
+ type Output = Self;
-/// Make a type castable from a value.
-#[macro_export]
-#[doc(hidden)]
-macro_rules! __castable {
- ($type:ty: $inner:ty) => {
- impl $crate::model::Cast<$crate::model::Value> for $type {
- fn is(value: &$crate::model::Value) -> bool {
- <$inner>::is(value)
+ fn add(self, rhs: Self) -> Self {
+ Self::Union(match (self, rhs) {
+ (Self::Union(mut lhs), Self::Union(rhs)) => {
+ lhs.extend(rhs);
+ lhs
}
-
- fn cast(value: $crate::model::Value) -> $crate::diag::StrResult<Self> {
- <$inner>::cast(value).map(Self)
+ (Self::Union(mut lhs), rhs) => {
+ lhs.push(rhs);
+ lhs
}
- }
- };
-
- (
- $type:ty,
- Expected: $expected:expr,
- $($pattern:pat => $out:expr,)*
- $(@$dyn_in:ident: $dyn_type:ty => $dyn_out:expr,)*
- ) => {
- #[allow(unreachable_patterns)]
- impl $crate::model::Cast<$crate::model::Value> for $type {
- fn is(value: &$crate::model::Value) -> bool {
- #[allow(unused_variables)]
- match value {
- $($pattern => true,)*
- $crate::model::Value::Dyn(dynamic) => {
- false $(|| dynamic.is::<$dyn_type>())*
- }
- _ => false,
- }
- }
-
- fn cast(value: $crate::model::Value) -> $crate::diag::StrResult<Self> {
- let found = match value {
- $($pattern => return Ok($out),)*
- $crate::model::Value::Dyn(dynamic) => {
- $(if let Some($dyn_in) = dynamic.downcast::<$dyn_type>() {
- return Ok($dyn_out);
- })*
- dynamic.type_name()
- }
- v => v.type_name(),
- };
-
- Err($crate::util::format_eco!(
- "expected {}, found {}",
- $expected,
- found,
- ))
+ (lhs, Self::Union(mut rhs)) => {
+ rhs.insert(0, lhs);
+ rhs
}
- }
- };
+ (lhs, rhs) => vec![lhs, rhs],
+ })
+ }
}
-#[doc(inline)]
-pub use crate::__castable as castable;
-
impl Cast for Value {
fn is(_: &Value) -> bool {
true
@@ -115,6 +122,10 @@ impl Cast for Value {
fn cast(value: Value) -> StrResult<Self> {
Ok(value)
}
+
+ fn describe() -> CastInfo {
+ CastInfo::Any
+ }
}
impl<T: Cast> Cast<Spanned<Value>> for T {
@@ -125,6 +136,10 @@ impl<T: Cast> Cast<Spanned<Value>> for T {
fn cast(value: Spanned<Value>) -> StrResult<Self> {
T::cast(value.v)
}
+
+ fn describe() -> CastInfo {
+ T::describe()
+ }
}
impl<T: Cast> Cast<Spanned<Value>> for Spanned<T> {
@@ -136,14 +151,64 @@ impl<T: Cast> Cast<Spanned<Value>> for Spanned<T> {
let span = value.span;
T::cast(value.v).map(|t| Spanned::new(t, span))
}
+
+ fn describe() -> CastInfo {
+ T::describe()
+ }
+}
+
+castable! {
+ Dir: "direction",
+}
+
+castable! {
+ GenAlign: "alignment",
+}
+
+castable! {
+ Regex: "regular expression",
+}
+
+castable! {
+ Selector: "selector",
+ text: EcoString => Self::text(&text),
+ label: Label => Self::Label(label),
+ func: Func => func.select(None)?,
+ regex: Regex => Self::Regex(regex),
+}
+
+castable! {
+ Axes<GenAlign>: "2d alignment",
+}
+
+castable! {
+ PartialStroke: "stroke",
+ thickness: Length => Self {
+ paint: Smart::Auto,
+ thickness: Smart::Custom(thickness),
+ },
+ color: Color => Self {
+ paint: Smart::Custom(color.into()),
+ thickness: Smart::Auto,
+ },
+}
+
+castable! {
+ u32,
+ int: i64 => int.try_into().map_err(|_| {
+ if int < 0 {
+ "number must be at least zero"
+ } else {
+ "number too large"
+ }
+ })?,
}
castable! {
usize,
- Expected: "non-negative integer",
- Value::Int(int) => int.try_into().map_err(|_| {
+ int: i64 => int.try_into().map_err(|_| {
if int < 0 {
- "must be at least zero"
+ "number must be at least zero"
} else {
"number too large"
}
@@ -152,12 +217,11 @@ castable! {
castable! {
NonZeroUsize,
- Expected: "positive integer",
- Value::Int(int) => int
+ int: i64 => int
.try_into()
.and_then(|int: usize| int.try_into())
.map_err(|_| if int <= 0 {
- "must be positive"
+ "number must be positive"
} else {
"number too large"
})?,
@@ -165,41 +229,23 @@ castable! {
castable! {
Paint,
- Expected: "color",
- Value::Color(color) => Paint::Solid(color),
+ color: Color => Self::Solid(color),
}
castable! {
EcoString,
- Expected: "string",
- Value::Str(str) => str.into(),
+ string: Str => string.into(),
}
castable! {
String,
- Expected: "string",
- Value::Str(string) => string.into(),
-}
-
-dynamic! {
- Regex: "regular expression",
-}
-
-dynamic! {
- Selector: "selector",
- Value::Str(text) => Self::text(&text),
- Value::Label(label) => Self::Label(label),
- Value::Func(func) => func.select(None)?,
- @regex: Regex => Self::Regex(regex.clone()),
+ string: Str => string.into(),
}
castable! {
Transform,
- Expected: "content or function",
- Value::None => Self::Content(Content::empty()),
- Value::Str(text) => Self::Content(item!(text)(text.into())),
- Value::Content(content) => Self::Content(content),
- Value::Func(func) => {
+ content: Content => Self::Content(content),
+ func: Func => {
if func.argc().map_or(false, |count| count != 1) {
Err("function must have exactly one parameter")?
}
@@ -207,45 +253,19 @@ castable! {
},
}
-dynamic! {
- Dir: "direction",
-}
-
-dynamic! {
- GenAlign: "alignment",
-}
-
-dynamic! {
- Axes<GenAlign>: "2d alignment",
-}
-
castable! {
Axes<Option<GenAlign>>,
- Expected: "1d or 2d alignment",
- @align: GenAlign => {
+ align: GenAlign => {
let mut aligns = Axes::default();
- aligns.set(align.axis(), Some(*align));
+ aligns.set(align.axis(), Some(align));
aligns
},
- @aligns: Axes<GenAlign> => aligns.map(Some),
-}
-
-dynamic! {
- PartialStroke: "stroke",
- Value::Length(thickness) => Self {
- paint: Smart::Auto,
- thickness: Smart::Custom(thickness),
- },
- Value::Color(color) => Self {
- paint: Smart::Custom(color.into()),
- thickness: Smart::Auto,
- },
+ aligns: Axes<GenAlign> => aligns.map(Some),
}
castable! {
Axes<Rel<Length>>,
- Expected: "array of two relative lengths",
- Value::Array(array) => {
+ array: Array => {
let mut iter = array.into_iter();
match (iter.next(), iter.next(), iter.next()) {
(Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?),
@@ -256,142 +276,124 @@ castable! {
castable! {
Location,
- Expected: "dictionary with `page`, `x`, and `y` keys",
- Value::Dict(dict) => {
- let page = dict.get("page")?.clone().cast()?;
- let x: Length = dict.get("x")?.clone().cast()?;
- let y: Length = dict.get("y")?.clone().cast()?;
+ mut dict: Dict => {
+ let page = dict.take("page")?.cast()?;
+ let x: Length = dict.take("x")?.cast()?;
+ let y: Length = dict.take("y")?.cast()?;
+ dict.finish(&["page", "x", "y"])?;
Self { page, pos: Point::new(x.abs, y.abs) }
},
}
castable! {
Destination,
- Expected: "string or dictionary with `page`, `x`, and `y` keys",
- Value::Str(string) => Self::Url(string.into()),
- v @ Value::Dict(_) => Self::Internal(v.cast()?),
+ loc: Location => Self::Internal(loc),
+ string: EcoString => Self::Url(string),
}
castable! {
FontStyle,
- Expected: "string",
- Value::Str(string) => match string.as_str() {
- "normal" => Self::Normal,
- "italic" => Self::Italic,
- "oblique" => Self::Oblique,
- _ => Err(r#"expected "normal", "italic" or "oblique""#)?,
- },
+ /// The default style.
+ "normal" => Self::Normal,
+ /// A cursive style.
+ "italic" => Self::Italic,
+ /// A slanted style.
+ "oblique" => Self::Oblique,
}
castable! {
FontWeight,
- Expected: "integer or string",
- Value::Int(v) => Self::from_number(v.clamp(0, u16::MAX as i64) as u16),
- Value::Str(string) => match string.as_str() {
- "thin" => Self::THIN,
- "extralight" => Self::EXTRALIGHT,
- "light" => Self::LIGHT,
- "regular" => Self::REGULAR,
- "medium" => Self::MEDIUM,
- "semibold" => Self::SEMIBOLD,
- "bold" => Self::BOLD,
- "extrabold" => Self::EXTRABOLD,
- "black" => Self::BLACK,
- _ => Err("unknown font weight")?,
- },
+ v: i64 => Self::from_number(v.clamp(0, u16::MAX as i64) as u16),
+ /// Thin weight (100).
+ "thin" => Self::THIN,
+ /// Extra light weight (200).
+ "extralight" => Self::EXTRALIGHT,
+ /// Light weight (300).
+ "light" => Self::LIGHT,
+ /// Regular weight (400).
+ "regular" => Self::REGULAR,
+ /// Medium weight (500).
+ "medium" => Self::MEDIUM,
+ /// Semibold weight (600).
+ "semibold" => Self::SEMIBOLD,
+ /// Bold weight (700).
+ "bold" => Self::BOLD,
+ /// Extrabold weight (800).
+ "extrabold" => Self::EXTRABOLD,
+ /// Black weight (900).
+ "black" => Self::BLACK,
}
castable! {
FontStretch,
- Expected: "ratio",
- Value::Ratio(v) => Self::from_ratio(v.get() as f32),
+ v: Ratio => Self::from_ratio(v.get() as f32),
}
castable! {
Lang,
- Expected: "string",
- Value::Str(string) => Self::from_str(&string)?,
+ string: EcoString => Self::from_str(&string)?,
}
castable! {
Region,
- Expected: "string",
- Value::Str(string) => Self::from_str(&string)?,
+ string: EcoString => Self::from_str(&string)?,
}
-impl<T: Cast> Cast for Option<T> {
+/// Castable from [`Value::None`].
+pub struct NoneValue;
+
+impl Cast for NoneValue {
fn is(value: &Value) -> bool {
- matches!(value, Value::None) || T::is(value)
+ matches!(value, Value::None)
}
fn cast(value: Value) -> StrResult<Self> {
match value {
- Value::None => Ok(None),
- v => T::cast(v).map(Some).map_err(|msg| with_alternative(msg, "none")),
+ Value::None => Ok(Self),
+ _ => <Self as Cast>::error(value),
}
}
+
+ fn describe() -> CastInfo {
+ CastInfo::Type("none")
+ }
}
-/// A value that can be automatically determined.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
-pub enum Smart<T> {
- /// The value should be determined smartly based on the circumstances.
- Auto,
- /// A specific value.
- Custom(T),
-}
-
-impl<T> Smart<T> {
- /// Map the contained custom value with `f`.
- pub fn map<F, U>(self, f: F) -> Smart<U>
- where
- F: FnOnce(T) -> U,
- {
- match self {
- Self::Auto => Smart::Auto,
- Self::Custom(x) => Smart::Custom(f(x)),
- }
+impl<T: Cast> Cast for Option<T> {
+ fn is(value: &Value) -> bool {
+ matches!(value, Value::None) || T::is(value)
}
- /// Keeps `self` if it contains a custom value, otherwise returns `other`.
- pub fn or(self, other: Smart<T>) -> Self {
- match self {
- Self::Custom(x) => Self::Custom(x),
- Self::Auto => other,
+ fn cast(value: Value) -> StrResult<Self> {
+ match value {
+ Value::None => Ok(None),
+ v if T::is(&v) => Ok(Some(T::cast(v)?)),
+ _ => <Self as Cast>::error(value),
}
}
- /// Returns the contained custom value or a provided default value.
- pub fn unwrap_or(self, default: T) -> T {
- match self {
- Self::Auto => default,
- Self::Custom(x) => x,
- }
+ fn describe() -> CastInfo {
+ T::describe() + CastInfo::Type("none")
}
+}
- /// Returns the contained custom value or computes a default value.
- pub fn unwrap_or_else<F>(self, f: F) -> T
- where
- F: FnOnce() -> T,
- {
- match self {
- Self::Auto => f(),
- Self::Custom(x) => x,
- }
+/// Castable from [`Value::Auto`].
+pub struct AutoValue;
+
+impl Cast for AutoValue {
+ fn is(value: &Value) -> bool {
+ matches!(value, Value::Auto)
}
- /// Returns the contained custom value or the default value.
- pub fn unwrap_or_default(self) -> T
- where
- T: Default,
- {
- self.unwrap_or_else(T::default)
+ fn cast(value: Value) -> StrResult<Self> {
+ match value {
+ Value::Auto => Ok(Self),
+ _ => <Self as Cast>::error(value),
+ }
}
-}
-impl<T> Default for Smart<T> {
- fn default() -> Self {
- Self::Auto
+ fn describe() -> CastInfo {
+ CastInfo::Type("auto")
}
}
@@ -403,11 +405,14 @@ impl<T: Cast> Cast for Smart<T> {
fn cast(value: Value) -> StrResult<Self> {
match value {
Value::Auto => Ok(Self::Auto),
- v => T::cast(v)
- .map(Self::Custom)
- .map_err(|msg| with_alternative(msg, "auto")),
+ v if T::is(&v) => Ok(Self::Custom(T::cast(v)?)),
+ _ => <Self as Cast>::error(value),
}
}
+
+ fn describe() -> CastInfo {
+ T::describe() + CastInfo::Type("auto")
+ }
}
impl<T> Cast for Sides<T>
@@ -420,7 +425,7 @@ where
fn cast(mut value: Value) -> StrResult<Self> {
if let Value::Dict(dict) = &mut value {
- let mut take = |key| dict.take(key).map(T::cast).transpose();
+ let mut take = |key| dict.take(key).ok().map(T::cast).transpose();
let rest = take("rest")?;
let x = take("x")?.or(rest);
@@ -432,22 +437,19 @@ where
bottom: take("bottom")?.or(y),
};
- if let Some((key, _)) = dict.iter().next() {
- return Err(format_eco!("unexpected key {key:?}"));
- }
+ dict.finish(&["left", "top", "right", "bottom", "x", "y", "rest"])?;
Ok(sides.map(Option::unwrap_or_default))
+ } else if T::is(&value) {
+ Ok(Self::splat(T::cast(value)?))
} else {
- T::cast(value).map(Self::splat).map_err(|msg| {
- with_alternative(
- msg,
- "dictionary with any of \
- `left`, `top`, `right`, `bottom`, \
- `x`, `y`, or `rest` as keys",
- )
- })
+ <Self as Cast>::error(value)
}
}
+
+ fn describe() -> CastInfo {
+ T::describe() + CastInfo::Type("dictionary")
+ }
}
impl<T> Cast for Corners<T>
@@ -460,7 +462,7 @@ where
fn cast(mut value: Value) -> StrResult<Self> {
if let Value::Dict(dict) = &mut value {
- let mut take = |key| dict.take(key).map(T::cast).transpose();
+ let mut take = |key| dict.take(key).ok().map(T::cast).transpose();
let rest = take("rest")?;
let left = take("left")?.or(rest);
@@ -474,20 +476,27 @@ where
bottom_left: take("bottom-left")?.or(bottom).or(left),
};
- if let Some((key, _)) = dict.iter().next() {
- return Err(format_eco!("unexpected key {key:?}"));
- }
+ dict.finish(&[
+ "top-left",
+ "top-right",
+ "bottom-right",
+ "bottom-left",
+ "left",
+ "top",
+ "right",
+ "bottom",
+ "rest",
+ ])?;
Ok(corners.map(Option::unwrap_or_default))
+ } else if T::is(&value) {
+ Ok(Self::splat(T::cast(value)?))
} else {
- T::cast(value).map(Self::splat).map_err(|msg| {
- with_alternative(
- msg,
- "dictionary with any of \
- `top-left`, `top-right`, `bottom-right`, `bottom-left`, \
- `left`, `top`, `right`, `bottom`, or `rest` as keys",
- )
- })
+ <Self as Cast>::error(value)
}
}
+
+ fn describe() -> CastInfo {
+ T::describe() + CastInfo::Type("dictionary")
+ }
}