appcore_filemaker/
source_layout.rs1use std::collections::{BTreeMap, BTreeSet};
14
15use serde::{Deserialize, Serialize};
16
17use crate::{
18 Distribution, ElementSource, ErrorCode, FileMakerError, LayoutMode, Length, Result, Unit,
19};
20
21#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
23#[serde(deny_unknown_fields)]
24pub struct ExclusionSource {
25 pub x: Length,
27 pub y: Length,
29 pub width: Length,
31 pub height: Length,
33 #[serde(default = "default_exclusion_group")]
35 pub group: String,
36 #[serde(default)]
38 pub collides_with: Vec<String>,
39}
40
41pub(crate) fn validate_exclusions(
42 exclusions: &BTreeMap<String, ExclusionSource>,
43 max_exclusions: usize,
44) -> Result<()> {
45 if exclusions.len() > max_exclusions {
46 return Err(limit_error(
47 "exclusion count exceeds configured element limit",
48 ));
49 }
50 for (name, exclusion) in exclusions {
51 validate_name("exclusion", name, 118)?;
52 validate_name("exclusion group", &exclusion.group, 128)?;
53 if exclusion.collides_with.len() > 64 {
54 return Err(limit_error("exclusion collision-group list exceeds 64"));
55 }
56 for group in &exclusion.collides_with {
57 validate_name("exclusion collision group", group, 128)?;
58 }
59 if [exclusion.x, exclusion.y, exclusion.width, exclusion.height].contains(&Length::Auto) {
60 return Err(schema_error("exclusion geometry cannot be auto"));
61 }
62 }
63 Ok(())
64}
65
66pub(crate) fn convert_exclusions(
67 exclusions: &BTreeMap<String, ExclusionSource>,
68) -> BTreeMap<String, crate::ExclusionIr> {
69 exclusions
70 .iter()
71 .map(|(name, source)| {
72 (
73 name.clone(),
74 crate::ExclusionIr {
75 x: source.x,
76 y: source.y,
77 width: source.width,
78 height: source.height,
79 group: source.group.clone(),
80 collides_with: source
81 .collides_with
82 .iter()
83 .cloned()
84 .collect::<BTreeSet<_>>(),
85 },
86 )
87 })
88 .collect()
89}
90
91fn default_exclusion_group() -> String {
92 "exclusion".to_owned()
93}
94
95fn validate_name(label: &str, value: &str, max_bytes: usize) -> Result<()> {
96 if value.is_empty()
97 || value.len() > max_bytes
98 || !value
99 .bytes()
100 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_'))
101 {
102 return Err(schema_error(format!("{label} name is invalid")));
103 }
104 Ok(())
105}
106
107fn schema_error(message: impl Into<String>) -> FileMakerError {
108 FileMakerError::new(ErrorCode::SchemaField, message)
109}
110
111fn limit_error(message: impl Into<String>) -> FileMakerError {
112 FileMakerError::new(ErrorCode::LimitExceeded, message)
113}
114
115pub(crate) fn default_gap() -> Length {
116 Length::Absolute(Unit::ZERO)
117}
118
119pub(crate) fn validate_layout_source(element: &ElementSource) -> Result<()> {
120 element.constraints.validate().map_err(|error| {
121 FileMakerError::new(ErrorCode::SchemaField, error.message()).at(element.id.clone())
122 })?;
123 if element.align_x.is_some() && element.x.is_some()
124 || element.align_y.is_some() && element.y.is_some()
125 {
126 return Err(FileMakerError::new(
127 ErrorCode::SchemaField,
128 "aligned axes cannot also declare an explicit coordinate",
129 )
130 .at(element.id.clone()));
131 }
132 if element.distribute != Distribution::Start && element.layout == LayoutMode::Absolute {
133 return Err(FileMakerError::new(
134 ErrorCode::SchemaField,
135 "distribution requires flow_vertical or flow_horizontal layout",
136 )
137 .at(element.id.clone()));
138 }
139 Ok(())
140}