use std::{rc::Rc, str::FromStr};
use orrery_core::{
color::Color,
draw::{
ActivationBoxDefinition, ArrowDefinition, FragmentDefinition, NoteDefinition,
ShapeDefinition, StrokeCap, StrokeDefinition, StrokeJoin, StrokeStyle, TextDefinition,
},
geometry::Insets,
identifier::Id,
};
use crate::{
error::{Diagnostic, ErrorCode, Result as DiagnosticResult},
parser_types,
};
#[derive(Debug, Clone)]
pub enum DrawDefinition {
Shape(Rc<Box<dyn ShapeDefinition>>),
Arrow(Rc<ArrowDefinition>),
Fragment(Rc<FragmentDefinition>),
Note(Rc<NoteDefinition>),
ActivationBox(Rc<ActivationBoxDefinition>),
Stroke(Rc<StrokeDefinition>),
Text(Rc<TextDefinition>),
}
#[derive(Debug, Clone)]
pub struct TypeDefinition {
id: Id,
draw_definition: DrawDefinition,
}
impl TypeDefinition {
fn new(id: Id, draw_definition: DrawDefinition) -> Self {
Self {
id,
draw_definition,
}
}
pub fn new_shape(id: Id, shape_definition: Rc<Box<dyn ShapeDefinition>>) -> Self {
Self::new(id, DrawDefinition::Shape(shape_definition))
}
pub fn new_arrow(id: Id, arrow_definition: Rc<ArrowDefinition>) -> Self {
Self::new(id, DrawDefinition::Arrow(arrow_definition))
}
pub fn new_fragment(id: Id, fragment_definition: Rc<FragmentDefinition>) -> Self {
Self::new(id, DrawDefinition::Fragment(fragment_definition))
}
pub fn new_note(id: Id, note_definition: Rc<NoteDefinition>) -> Self {
Self::new(id, DrawDefinition::Note(note_definition))
}
pub fn new_activation_box(
id: Id,
activation_box_definition: Rc<ActivationBoxDefinition>,
) -> Self {
Self::new(id, DrawDefinition::ActivationBox(activation_box_definition))
}
pub fn new_stroke(id: Id, stroke_definition: StrokeDefinition) -> Self {
Self::new(id, DrawDefinition::Stroke(Rc::new(stroke_definition)))
}
pub fn new_text(id: Id, text_definition: TextDefinition) -> Self {
Self::new(id, DrawDefinition::Text(Rc::new(text_definition)))
}
pub fn id(&self) -> Id {
self.id
}
pub fn draw_definition(&self) -> &DrawDefinition {
&self.draw_definition
}
pub fn shape_definition(&self) -> Result<&Rc<Box<dyn ShapeDefinition>>, String> {
match &self.draw_definition {
DrawDefinition::Shape(shape) => Ok(shape),
_ => Err(format!("Type '{}' is not a shape type", self.id)),
}
}
pub fn arrow_definition(&self) -> Result<&Rc<ArrowDefinition>, String> {
match &self.draw_definition {
DrawDefinition::Arrow(arrow) => Ok(arrow),
_ => Err(format!("Type '{}' is not an arrow type", self.id)),
}
}
pub fn fragment_definition(&self) -> Result<&Rc<FragmentDefinition>, String> {
match &self.draw_definition {
DrawDefinition::Fragment(fragment) => Ok(fragment),
_ => Err(format!("Type '{}' is not a fragment type", self.id)),
}
}
pub fn note_definition(&self) -> Result<&Rc<NoteDefinition>, String> {
match &self.draw_definition {
DrawDefinition::Note(note) => Ok(note),
_ => Err(format!("Type '{}' is not a note type", self.id)),
}
}
pub fn activation_box_definition(&self) -> Result<&Rc<ActivationBoxDefinition>, String> {
match &self.draw_definition {
DrawDefinition::ActivationBox(activation_box) => Ok(activation_box),
_ => Err(format!("Type '{}' is not an activation box type", self.id)),
}
}
pub fn stroke_definition(&self) -> Result<&Rc<StrokeDefinition>, String> {
match &self.draw_definition {
DrawDefinition::Stroke(stroke) => Ok(stroke),
_ => Err(format!("Type '{}' is not a stroke type", self.id)),
}
}
pub fn text_definition_from_draw(&self) -> Result<&Rc<TextDefinition>, String> {
match &self.draw_definition {
DrawDefinition::Text(text) => Ok(text),
_ => Err(format!("Type '{}' is not a text type", self.id)),
}
}
}
pub struct TextAttributeExtractor;
impl TextAttributeExtractor {
pub fn extract_text_attributes(
text_def: &mut TextDefinition,
attrs: &[parser_types::Attribute],
) -> DiagnosticResult<()> {
for attr in attrs {
Self::extract_single_attribute(text_def, attr)?;
}
Ok(())
}
fn extract_single_attribute(
text_def: &mut TextDefinition,
attr: &parser_types::Attribute,
) -> DiagnosticResult<()> {
let name = attr.name.inner();
let value = &attr.value;
match *name {
"font_size" => {
let val = value.as_u16().map_err(|_| {
Diagnostic::error(format!("invalid `font_size` value `{value}`"))
.with_code(ErrorCode::E302)
.with_label(attr.span(), "invalid number")
.with_help("font size must be a positive integer")
})?;
text_def.set_font_size(val);
Ok(())
}
"font_family" => {
text_def.set_font_family(value.as_str().map_err(|err| {
Diagnostic::error(err.to_string())
.with_code(ErrorCode::E302)
.with_label(attr.span(), "invalid font family")
.with_help("font family must be a string value")
})?);
Ok(())
}
"background_color" => {
let val = Color::new(value.as_str().map_err(|err| {
Diagnostic::error(err.to_string())
.with_code(ErrorCode::E302)
.with_label(attr.span(), "invalid color value")
.with_help("color values must be strings")
})?)
.map_err(|err| {
Diagnostic::error(format!("invalid `background_color`: {err}"))
.with_code(ErrorCode::E302)
.with_label(attr.span(), "invalid color")
.with_help("use a CSS color")
})?;
text_def.set_background_color(Some(val));
Ok(())
}
"padding" => {
let val = value.as_float().map_err(|err| {
Diagnostic::error(format!("invalid `padding` value: {err}"))
.with_code(ErrorCode::E302)
.with_label(attr.span(), "invalid number")
.with_help("text padding must be a positive number")
})?;
text_def.set_padding(Insets::uniform(val));
Ok(())
}
"color" => {
let val = Color::new(value.as_str().map_err(|err| {
Diagnostic::error(err.to_string())
.with_code(ErrorCode::E302)
.with_label(attr.span(), "invalid color value")
.with_help("color values must be strings")
})?)
.map_err(|err| {
Diagnostic::error(format!("invalid `color`: {err}"))
.with_code(ErrorCode::E302)
.with_label(attr.span(), "invalid color")
.with_help("use a CSS color")
})?;
text_def.set_color(Some(val));
Ok(())
}
name => Err(Diagnostic::error(format!("unknown text attribute `{name}`"))
.with_code(ErrorCode::E303)
.with_label(attr.span(), "unknown attribute")
.with_help(
"valid text attributes are: `font_size`, `font_family`, `background_color`, `padding`, `color`",
)),
}
}
}
pub struct StrokeAttributeExtractor;
impl StrokeAttributeExtractor {
pub fn extract_stroke_attributes(
stroke_def: &mut StrokeDefinition,
attrs: &[parser_types::Attribute],
) -> DiagnosticResult<()> {
for attr in attrs {
Self::extract_single_attribute(stroke_def, attr)?;
}
Ok(())
}
fn extract_single_attribute(
stroke_def: &mut StrokeDefinition,
attr: &parser_types::Attribute,
) -> DiagnosticResult<()> {
let name = *attr.name.inner();
let value = &attr.value;
match name {
"color" => {
let color_str = value.as_str().map_err(|err| {
Diagnostic::error(err.to_string())
.with_code(ErrorCode::E302)
.with_label(attr.span(), "invalid color value")
.with_help("color values must be strings")
})?;
let val = Color::new(color_str).map_err(|err| {
Diagnostic::error(format!("invalid stroke `color`: {err}"))
.with_code(ErrorCode::E302)
.with_label(attr.span(), "invalid color")
.with_help("use a CSS color")
})?;
stroke_def.set_color(val);
Ok(())
}
"width" => {
let val = value.as_float().map_err(|err| {
Diagnostic::error(format!("invalid stroke `width` value: {err}"))
.with_code(ErrorCode::E302)
.with_label(attr.span(), "invalid number")
.with_help("width must be a positive number")
})?;
stroke_def.set_width(val);
Ok(())
}
"style" => {
let style_str = value.as_str().map_err(|err| {
Diagnostic::error(err.to_string())
.with_code(ErrorCode::E302)
.with_label(attr.span(), "invalid stroke style value")
.with_help("stroke style must be a string")
})?;
let style = StrokeStyle::from_str(style_str).map_err(|err| {
Diagnostic::error(err)
.with_code(ErrorCode::E302)
.with_label(attr.span(), "invalid stroke style")
.with_help("use a valid style name or dasharray pattern")
})?;
stroke_def.set_style(style);
Ok(())
}
"cap" => {
let cap_str = value.as_str().map_err(|err| {
Diagnostic::error(err.to_string())
.with_code(ErrorCode::E302)
.with_label(attr.span(), "invalid stroke cap value")
.with_help("stroke cap must be a string")
})?;
let cap = StrokeCap::from_str(cap_str).map_err(|err| {
Diagnostic::error(err)
.with_code(ErrorCode::E302)
.with_label(attr.span(), "invalid stroke cap")
.with_help("valid values are: `butt`, `round`, `square`")
})?;
stroke_def.set_cap(cap);
Ok(())
}
"join" => {
let join_str = value.as_str().map_err(|err| {
Diagnostic::error(err.to_string())
.with_code(ErrorCode::E302)
.with_label(attr.span(), "invalid stroke join value")
.with_help("stroke join must be a string")
})?;
let join = StrokeJoin::from_str(join_str).map_err(|err| {
Diagnostic::error(err)
.with_code(ErrorCode::E302)
.with_label(attr.span(), "invalid stroke join")
.with_help("valid values are: `miter`, `round`, `bevel`")
})?;
stroke_def.set_join(join);
Ok(())
}
name => Err(
Diagnostic::error(format!("unknown stroke attribute `{name}`"))
.with_code(ErrorCode::E303)
.with_label(attr.span(), "unknown attribute")
.with_help(
"valid stroke attributes are: `color`, `width`, `style`, `cap`, `join`",
),
),
}
}
}
#[cfg(test)]
mod elaborate_tests {
use orrery_core::draw::RectangleDefinition;
use super::*;
use crate::span::{Span, Spanned};
#[test]
fn test_new_stroke_type() {
let stroke = StrokeDefinition::default();
let type_def = TypeDefinition::new_stroke(Id::new("TestStroke"), stroke);
assert_eq!(type_def.id(), "TestStroke");
assert!(type_def.stroke_definition().is_ok());
assert!(type_def.text_definition_from_draw().is_err());
}
#[test]
fn test_new_text_type() {
let text = TextDefinition::default();
let type_def = TypeDefinition::new_text(Id::new("TestText"), text);
assert_eq!(type_def.id(), "TestText");
assert!(type_def.text_definition_from_draw().is_ok());
assert!(type_def.stroke_definition().is_err());
}
#[test]
fn test_shape_type_has_text_definition() {
let type_def = TypeDefinition::new_shape(
Id::new("Rect"),
Rc::new(Box::new(RectangleDefinition::new())),
);
assert!(type_def.shape_definition().is_ok());
let shape_def = type_def.shape_definition().unwrap();
let _text = shape_def.text(); assert!(type_def.stroke_definition().is_err());
}
fn create_test_attribute(
name: &'static str,
value: parser_types::AttributeValue<'static>,
) -> parser_types::Attribute<'static> {
parser_types::Attribute {
name: Spanned::new(name, Span::default()),
value,
}
}
fn create_string_value(s: &str) -> parser_types::AttributeValue<'static> {
parser_types::AttributeValue::String(Spanned::new(s.to_string(), Span::default()))
}
fn create_float_value(f: f32) -> parser_types::AttributeValue<'static> {
parser_types::AttributeValue::Float(Spanned::new(f, Span::default()))
}
#[test]
fn test_text_attribute_extractor_all_attributes() {
let mut text_def = TextDefinition::new();
let attributes = vec![
create_test_attribute("font_size", create_float_value(16.0)),
create_test_attribute("font_family", create_string_value("Helvetica")),
create_test_attribute("background_color", create_string_value("red")),
create_test_attribute("padding", create_float_value(5.0)),
create_test_attribute("color", create_string_value("blue")),
];
let result = TextAttributeExtractor::extract_text_attributes(&mut text_def, &attributes);
assert!(result.is_ok());
}
#[test]
fn test_text_attribute_extractor_color_attribute() {
let mut text_def = TextDefinition::new();
let attributes = vec![create_test_attribute("color", create_string_value("red"))];
let result = TextAttributeExtractor::extract_text_attributes(&mut text_def, &attributes);
assert!(result.is_ok());
let mut text_def = TextDefinition::new();
let attributes = vec![create_test_attribute("color", create_float_value(255.0))];
let result = TextAttributeExtractor::extract_text_attributes(&mut text_def, &attributes);
assert!(result.is_err());
}
#[test]
fn test_text_attribute_extractor_empty_attributes() {
let mut text_def = TextDefinition::new();
let attributes = vec![];
let result = TextAttributeExtractor::extract_text_attributes(&mut text_def, &attributes);
assert!(result.is_ok());
}
#[test]
fn test_text_attribute_extractor_invalid_attribute_name() {
let mut text_def = TextDefinition::new();
let attributes = vec![
create_test_attribute("font_size", create_float_value(16.0)),
create_test_attribute("invalid_attribute", create_string_value("test")),
];
let result = TextAttributeExtractor::extract_text_attributes(&mut text_def, &attributes);
assert!(result.is_err());
if let Err(err) = result {
let error_message = err.to_string();
assert!(error_message.contains("unknown text attribute `invalid_attribute`"));
}
}
#[test]
fn test_text_attribute_extractor_invalid_value_types() {
let mut text_def = TextDefinition::new();
let attributes = vec![create_test_attribute(
"font_size",
create_string_value("not_a_number"),
)];
let result = TextAttributeExtractor::extract_text_attributes(&mut text_def, &attributes);
assert!(result.is_err());
let mut text_def = TextDefinition::new();
let attributes = vec![create_test_attribute(
"font_family",
create_float_value(123.0),
)];
let result = TextAttributeExtractor::extract_text_attributes(&mut text_def, &attributes);
assert!(result.is_err());
}
#[test]
fn test_stroke_attribute_extractor_all_attributes() {
let attrs = vec![
create_test_attribute("color", create_string_value("blue")),
create_test_attribute("width", create_float_value(2.5)),
create_test_attribute("style", create_string_value("dashed")),
create_test_attribute("cap", create_string_value("round")),
create_test_attribute("join", create_string_value("bevel")),
];
let mut stroke_def = StrokeDefinition::default();
let result = StrokeAttributeExtractor::extract_stroke_attributes(&mut stroke_def, &attrs);
assert!(result.is_ok());
assert_eq!(stroke_def.color().to_string(), "blue");
assert_eq!(stroke_def.width(), 2.5);
assert_eq!(*stroke_def.style(), StrokeStyle::Dashed);
assert_eq!(stroke_def.cap(), StrokeCap::Round);
assert_eq!(stroke_def.join(), StrokeJoin::Bevel);
}
#[test]
fn test_stroke_attribute_extractor_color_only() {
let attrs = vec![create_test_attribute("color", create_string_value("red"))];
let mut stroke_def = StrokeDefinition::default();
let result = StrokeAttributeExtractor::extract_stroke_attributes(&mut stroke_def, &attrs);
assert!(result.is_ok());
assert_eq!(stroke_def.color().to_string(), "red");
}
#[test]
fn test_stroke_attribute_extractor_invalid_attribute_name() {
let attrs = vec![create_test_attribute(
"invalid_attr",
create_string_value("value"),
)];
let mut stroke_def = StrokeDefinition::default();
let result = StrokeAttributeExtractor::extract_stroke_attributes(&mut stroke_def, &attrs);
assert!(result.is_err());
if let Err(err) = result {
let error_message = format!("{err}");
assert!(error_message.contains("unknown stroke attribute"));
assert!(error_message.contains("invalid_attr"));
}
}
#[test]
fn test_stroke_attribute_extractor_invalid_color() {
let attrs = vec![create_test_attribute(
"color",
create_string_value("not-a-valid-color-12345"),
)];
let mut stroke_def = StrokeDefinition::default();
let result = StrokeAttributeExtractor::extract_stroke_attributes(&mut stroke_def, &attrs);
assert!(result.is_err());
if let Err(err) = result {
let error_message = format!("{err}");
assert!(error_message.contains("invalid stroke `color`"));
}
}
#[test]
fn test_stroke_attribute_extractor_invalid_cap() {
let attrs = vec![create_test_attribute("cap", create_string_value("invalid"))];
let mut stroke_def = StrokeDefinition::default();
let result = StrokeAttributeExtractor::extract_stroke_attributes(&mut stroke_def, &attrs);
assert!(result.is_err());
if let Err(err) = result {
let error_message = format!("{err}");
assert!(error_message.contains("invalid stroke cap"));
}
}
#[test]
fn test_stroke_attribute_extractor_invalid_join() {
let attrs = vec![create_test_attribute(
"join",
create_string_value("invalid"),
)];
let mut stroke_def = StrokeDefinition::default();
let result = StrokeAttributeExtractor::extract_stroke_attributes(&mut stroke_def, &attrs);
assert!(result.is_err());
if let Err(err) = result {
let error_message = format!("{err}");
assert!(error_message.contains("invalid stroke join"));
}
}
#[test]
fn test_stroke_attribute_extractor_all_predefined_styles() {
let styles = vec![
("solid", StrokeStyle::Solid),
("dashed", StrokeStyle::Dashed),
("dotted", StrokeStyle::Dotted),
("dash-dot", StrokeStyle::DashDot),
("dash-dot-dot", StrokeStyle::DashDotDot),
];
for (style_str, expected_style) in styles {
let attrs = vec![create_test_attribute(
"style",
create_string_value(style_str),
)];
let mut stroke_def = StrokeDefinition::default();
let result =
StrokeAttributeExtractor::extract_stroke_attributes(&mut stroke_def, &attrs);
assert!(result.is_ok());
assert_eq!(*stroke_def.style(), expected_style);
}
}
}