1use std::collections::{BTreeMap, BTreeSet};
10use std::fmt;
11
12use links_notation::{LiNo, ParserConfig, parse_lino_to_links_with_config};
13
14use crate::grammar::{percent_decode_links_text, percent_encode_links_text};
15
16pub type DecoratorRecord = BTreeMap<String, String>;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
21pub enum DecoratorLevel {
22 Importer,
24 GrammarRule,
26 MergeDecision,
28 ConceptMapping,
30 Executor,
32 Recovery,
34 CstToAst,
36 Transformation,
38 Emitter,
40 TranslationRule,
42}
43
44pub const DECORATOR_LEVELS: [DecoratorLevel; 10] = [
46 DecoratorLevel::Importer,
47 DecoratorLevel::GrammarRule,
48 DecoratorLevel::MergeDecision,
49 DecoratorLevel::ConceptMapping,
50 DecoratorLevel::Executor,
51 DecoratorLevel::Recovery,
52 DecoratorLevel::CstToAst,
53 DecoratorLevel::Transformation,
54 DecoratorLevel::Emitter,
55 DecoratorLevel::TranslationRule,
56];
57
58impl DecoratorLevel {
59 #[must_use]
61 pub const fn as_str(self) -> &'static str {
62 match self {
63 Self::Importer => "importer",
64 Self::GrammarRule => "grammar-rule",
65 Self::MergeDecision => "merge-decision",
66 Self::ConceptMapping => "concept-mapping",
67 Self::Executor => "executor",
68 Self::Recovery => "recovery",
69 Self::CstToAst => "cst-to-ast",
70 Self::Transformation => "transformation",
71 Self::Emitter => "emitter",
72 Self::TranslationRule => "translation-rule",
73 }
74 }
75
76 #[must_use]
78 pub fn parse(name: &str) -> Option<Self> {
79 DECORATOR_LEVELS
80 .into_iter()
81 .find(|level| level.as_str() == name)
82 }
83}
84
85impl fmt::Display for DecoratorLevel {
86 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
87 formatter.write_str(self.as_str())
88 }
89}
90
91#[derive(Clone, Debug, PartialEq, Eq)]
93pub struct DecoratorError {
94 pub message: String,
96}
97
98impl DecoratorError {
99 #[must_use]
101 pub fn new(message: impl Into<String>) -> Self {
102 Self {
103 message: message.into(),
104 }
105 }
106}
107
108impl fmt::Display for DecoratorError {
109 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
110 formatter.write_str(&self.message)
111 }
112}
113
114impl std::error::Error for DecoratorError {}
115
116#[derive(Clone, Debug, PartialEq, Eq)]
118pub enum DecoratorAction {
119 Set {
121 field: String,
123 value: String,
125 },
126 Replace {
128 field: String,
130 from: String,
132 to: String,
134 },
135 Drop,
137}
138
139#[derive(Clone, Debug, PartialEq, Eq)]
142pub struct Decorator {
143 id: String,
144 level: DecoratorLevel,
145 order: i64,
146 when: Vec<(String, String)>,
147 actions: Vec<DecoratorAction>,
148}
149
150fn is_name(value: &str) -> bool {
151 !value.is_empty()
152 && value
153 .bytes()
154 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
155}
156
157impl Decorator {
158 pub fn new(
165 id: impl Into<String>,
166 level: DecoratorLevel,
167 order: i64,
168 when: Vec<(String, String)>,
169 actions: Vec<DecoratorAction>,
170 ) -> Result<Self, DecoratorError> {
171 let id = id.into();
172 if !is_name(&id) {
173 return Err(DecoratorError::new(format!("invalid decorator id {id:?}")));
174 }
175 if when.iter().any(|(field, _)| !is_name(field)) {
176 return Err(DecoratorError::new(format!(
177 "decorator {id} has an invalid condition"
178 )));
179 }
180 if actions.is_empty() {
181 return Err(DecoratorError::new(format!("decorator {id} has no action")));
182 }
183 for action in &actions {
184 match action {
185 DecoratorAction::Drop => {}
186 DecoratorAction::Set { field, .. } if is_name(field) => {}
187 DecoratorAction::Replace { field, from, .. } if is_name(field) => {
188 if from.is_empty() {
189 return Err(DecoratorError::new(format!(
190 "decorator {id} replaces an empty or non-string text in {field}"
191 )));
192 }
193 }
194 _ => {
195 return Err(DecoratorError::new(format!(
196 "decorator {id} has an action without a field"
197 )));
198 }
199 }
200 }
201 Ok(Self {
202 id,
203 level,
204 order,
205 when,
206 actions,
207 })
208 }
209
210 #[must_use]
212 pub fn id(&self) -> &str {
213 &self.id
214 }
215
216 #[must_use]
218 pub const fn level(&self) -> DecoratorLevel {
219 self.level
220 }
221
222 #[must_use]
224 pub const fn order(&self) -> i64 {
225 self.order
226 }
227
228 #[must_use]
230 pub fn when(&self) -> &[(String, String)] {
231 &self.when
232 }
233
234 #[must_use]
236 pub fn actions(&self) -> &[DecoratorAction] {
237 &self.actions
238 }
239
240 fn render(&self) -> String {
241 let mut parts = vec![
242 format!("decorator {}", self.id),
243 format!("(level {})", self.level),
244 format!("(order {})", self.order),
245 ];
246 if !self.when.is_empty() {
247 let pairs: Vec<String> = self
248 .when
249 .iter()
250 .map(|(field, value)| format!("({field} {})", percent_encode_links_text(value)))
251 .collect();
252 parts.push(format!("(when {})", pairs.join(" ")));
253 }
254 for action in &self.actions {
255 parts.push(match action {
256 DecoratorAction::Drop => "(drop)".to_owned(),
257 DecoratorAction::Set { field, value } => {
258 format!("(set {field} {})", percent_encode_links_text(value))
259 }
260 DecoratorAction::Replace { field, from, to } => format!(
261 "(replace {field} {} {})",
262 percent_encode_links_text(from),
263 percent_encode_links_text(to)
264 ),
265 });
266 }
267 format!("({})", parts.join(" "))
268 }
269}
270
271#[derive(Clone, Debug, Default, PartialEq, Eq)]
278pub struct DecoratorSet {
279 decorators: Vec<Decorator>,
280}
281
282impl DecoratorSet {
283 pub fn new(mut decorators: Vec<Decorator>) -> Result<Self, DecoratorError> {
289 let mut ids = BTreeSet::new();
290 for decorator in &decorators {
291 if !ids.insert(decorator.id.as_str()) {
292 return Err(DecoratorError::new(format!(
293 "duplicate decorator id {}",
294 decorator.id
295 )));
296 }
297 }
298 decorators.sort_by(|left, right| {
299 left.order
300 .cmp(&right.order)
301 .then_with(|| left.id.cmp(&right.id))
302 });
303 Ok(Self { decorators })
304 }
305
306 #[must_use]
308 pub const fn empty() -> Self {
309 Self {
310 decorators: Vec::new(),
311 }
312 }
313
314 pub fn from_lino(text: &str) -> Result<Self, DecoratorError> {
322 let statements = parse_lino_to_links_with_config(text, &ParserConfig::without_comments())
323 .map_err(|error| DecoratorError::new(error.to_string()))?;
324 Self::new(
325 statements
326 .iter()
327 .map(read_decorator)
328 .collect::<Result<_, _>>()?,
329 )
330 }
331
332 #[must_use]
334 pub fn decorators(&self) -> &[Decorator] {
335 &self.decorators
336 }
337
338 #[must_use]
340 pub const fn len(&self) -> usize {
341 self.decorators.len()
342 }
343
344 #[must_use]
346 pub const fn is_empty(&self) -> bool {
347 self.decorators.is_empty()
348 }
349
350 #[must_use]
352 pub fn ids(&self) -> Vec<&str> {
353 self.decorators.iter().map(Decorator::id).collect()
354 }
355
356 pub fn add(
362 &self,
363 decorators: impl IntoIterator<Item = Decorator>,
364 ) -> Result<Self, DecoratorError> {
365 Self::new(self.decorators.iter().cloned().chain(decorators).collect())
366 }
367
368 pub fn remove(&self, id: &str) -> Result<Self, DecoratorError> {
374 if !self.decorators.iter().any(|decorator| decorator.id == id) {
375 return Err(DecoratorError::new(format!("no decorator {id} to remove")));
376 }
377 Ok(Self {
378 decorators: self
379 .decorators
380 .iter()
381 .filter(|decorator| decorator.id != id)
382 .cloned()
383 .collect(),
384 })
385 }
386
387 pub fn for_level(&self, level: DecoratorLevel) -> impl Iterator<Item = &Decorator> {
389 self.decorators
390 .iter()
391 .filter(move |decorator| decorator.level == level)
392 }
393
394 #[must_use]
396 pub fn has(&self, level: DecoratorLevel) -> bool {
397 self.for_level(level).next().is_some()
398 }
399
400 #[must_use]
403 pub fn decorate(
404 &self,
405 level: DecoratorLevel,
406 record: &DecoratorRecord,
407 ) -> Option<DecoratorRecord> {
408 let mut current = record.clone();
409 for decorator in self.for_level(level) {
410 let applies = decorator
411 .when
412 .iter()
413 .all(|(field, value)| current.get(field).map_or("", String::as_str) == value);
414 if !applies {
415 continue;
416 }
417 for action in &decorator.actions {
418 match action {
419 DecoratorAction::Drop => return None,
420 DecoratorAction::Set { field, value } => {
421 current.insert(field.clone(), value.clone());
422 }
423 DecoratorAction::Replace { field, from, to } => {
424 let text = current
425 .get(field)
426 .map_or("", String::as_str)
427 .replace(from, to);
428 current.insert(field.clone(), text);
429 }
430 }
431 }
432 }
433 Some(current)
434 }
435
436 #[must_use]
438 pub fn decorate_all(
439 &self,
440 level: DecoratorLevel,
441 records: &[DecoratorRecord],
442 ) -> Vec<DecoratorRecord> {
443 records
444 .iter()
445 .filter_map(|record| self.decorate(level, record))
446 .collect()
447 }
448
449 #[must_use]
451 pub fn to_lino(&self) -> String {
452 self.decorators
453 .iter()
454 .fold(String::new(), |mut text, decorator| {
455 text.push_str(&decorator.render());
456 text.push('\n');
457 text
458 })
459 }
460}
461
462#[must_use]
464pub fn decorator_record<const N: usize>(fields: [(&str, &str); N]) -> DecoratorRecord {
465 fields
466 .into_iter()
467 .map(|(field, text)| (field.to_owned(), text.to_owned()))
468 .collect()
469}
470
471#[must_use]
473pub fn record_field<'a>(record: &'a DecoratorRecord, field: &str) -> &'a str {
474 record.get(field).map_or("", String::as_str)
475}
476
477type Node = LiNo<String>;
478
479fn word_of(node: &Node) -> Option<&str> {
480 match node {
481 LiNo::Ref(word) => Some(word),
482 LiNo::Link {
483 id: Some(word),
484 values,
485 } if values.is_empty() => Some(word),
486 LiNo::Link { .. } => None,
487 }
488}
489
490fn values_of(node: &Node) -> &[Node] {
491 match node {
492 LiNo::Link { values, .. } => values,
493 LiNo::Ref(_) => &[],
494 }
495}
496
497fn words(values: &[Node]) -> Result<Vec<&str>, DecoratorError> {
498 values
499 .iter()
500 .map(|value| {
501 word_of(value).ok_or_else(|| {
502 DecoratorError::new("a decorator field holds a nested link where a word belongs")
503 })
504 })
505 .collect()
506}
507
508fn unquote(text: &str, id: &str) -> Result<String, DecoratorError> {
509 percent_decode_links_text(text).map_err(|_| {
510 DecoratorError::new(format!(
511 "decorator {id} has a malformed percent-encoded value {text}"
512 ))
513 })
514}
515
516fn read_decorator(statement: &Node) -> Result<Decorator, DecoratorError> {
517 let shape = || {
518 DecoratorError::new("a decorator statement must read (decorator <id> (level <level>) …)")
519 };
520 let LiNo::Link { id: None, values } = statement else {
521 return Err(shape());
522 };
523 let [head, name, rest @ ..] = values.as_slice() else {
524 return Err(shape());
525 };
526 if word_of(head) != Some("decorator") {
527 return Err(shape());
528 }
529 let id = word_of(name).ok_or_else(shape)?.to_owned();
530 let mut level = None;
531 let mut order = 0;
532 let mut when = Vec::new();
533 let mut actions = Vec::new();
534 for part in rest {
535 let parts = values_of(part);
536 let keyword = parts.first().and_then(word_of);
537 let args = parts.get(1..).unwrap_or_default();
538 match keyword {
539 Some("level") => {
540 let name = words(args)?.first().copied().unwrap_or_default();
541 level = Some(DecoratorLevel::parse(name).ok_or_else(|| {
542 DecoratorError::new(format!("decorator {id} names unknown level {name:?}"))
543 })?);
544 }
545 Some("order") => {
546 let text = words(args)?.first().copied().unwrap_or_default();
547 order = text
548 .parse()
549 .ok()
550 .filter(|_| {
551 let digits = text.strip_prefix('-').unwrap_or(text);
552 !digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit())
553 })
554 .ok_or_else(|| {
555 DecoratorError::new(format!("decorator {id} has a non-integer order"))
556 })?;
557 }
558 Some("when") => {
559 for pair in args {
560 let [field, value] = words(values_of(pair))?[..] else {
561 return Err(DecoratorError::new(format!(
562 "decorator {id} has an invalid condition"
563 )));
564 };
565 when.push((field.to_owned(), unquote(value, &id)?));
566 }
567 }
568 Some("set") => {
569 let [field, value] = words(args)?[..] else {
570 return Err(DecoratorError::new(format!(
571 "decorator {id} has an action without a field"
572 )));
573 };
574 actions.push(DecoratorAction::Set {
575 field: field.to_owned(),
576 value: unquote(value, &id)?,
577 });
578 }
579 Some("replace") => {
580 let [field, from, to] = words(args)?[..] else {
581 return Err(DecoratorError::new(format!(
582 "decorator {id} replaces an empty or non-string text"
583 )));
584 };
585 actions.push(DecoratorAction::Replace {
586 field: field.to_owned(),
587 from: unquote(from, &id)?,
588 to: unquote(to, &id)?,
589 });
590 }
591 Some("drop") => actions.push(DecoratorAction::Drop),
592 other => {
593 return Err(DecoratorError::new(format!(
594 "decorator {id} has an unknown part {other:?}"
595 )));
596 }
597 }
598 }
599 let level =
600 level.ok_or_else(|| DecoratorError::new(format!("decorator {id} names no level")))?;
601 Decorator::new(id, level, order, when, actions)
602}