use crate::builder::assign_group_ids;
use crate::document::{Document, Stage, StageItem, Sub, SubTo, Tests};
use crate::error::{Error, Result};
use crate::item::Item;
use crate::stdlib::{Funcall, Prim};
use std::collections::BTreeMap;
use std::rc::Rc;
pub fn reverse_name(name: &str) -> String {
let mut parts: Vec<&str> = name.split('-').collect();
if parts.len() >= 4 {
parts.swap(2, 3);
let swapped = parts.join("-");
if swapped != name {
return swapped;
}
}
let stripped = name.replace("-reverse", "");
if stripped != name {
stripped
} else {
format!("{name}-reverse")
}
}
pub fn reverse_document(doc: &Document) -> Result<Document> {
let mut stages = BTreeMap::new();
for (name, stage) in &doc.stages {
stages.insert(name.clone(), reverse_stage(stage)?);
}
let mut dep_aliases = BTreeMap::new();
for (alias, dep) in &doc.dep_aliases {
dep_aliases.insert(alias.clone(), Rc::new(reverse_document(dep)?));
}
let mut imports = Vec::with_capacity(doc.imports.len());
for dep in &doc.imports {
imports.push(Rc::new(reverse_document(dep)?));
}
Ok(Document {
name: reverse_name(&doc.name),
system_code: doc.system_code.clone(),
aliases: doc.aliases.clone(),
stages,
dep_aliases,
imports,
tests: Tests {
data: doc
.tests
.data
.iter()
.map(|(a, b)| (b.clone(), a.clone()))
.collect(),
},
metadata: reverse_metadata(&doc.metadata),
})
}
fn reverse_metadata(metadata: &[(String, String)]) -> Vec<(String, String)> {
let src = metadata
.iter()
.find(|(k, _)| k == "source_script")
.map(|(_, v)| v.clone());
let dst = metadata
.iter()
.find(|(k, _)| k == "destination_script")
.map(|(_, v)| v.clone());
let mut out = metadata.to_vec();
for (k, v) in out.iter_mut() {
match k.as_str() {
"source_script" => {
if let Some(d) = &dst {
*v = d.clone();
}
}
"destination_script" => {
if let Some(s) = &src {
*v = s.clone();
}
}
_ => {}
}
}
out
}
pub fn reverse_stage(stage: &Stage) -> Result<Stage> {
let mut items: Vec<StageItem> = Vec::with_capacity(stage.items.len());
for item in stage.items.iter().rev() {
items.push(reverse_stage_item(item)?);
}
Ok(Stage {
name: stage.name.clone(),
items,
reverse_run: toggle(stage.reverse_run),
})
}
fn toggle(run: Option<bool>) -> Option<bool> {
run.map(|b| !b)
}
fn reverse_stage_item(item: &StageItem) -> Result<StageItem> {
Ok(match item {
StageItem::Parallel(rules) => {
let mut out = Vec::with_capacity(rules.len());
for rule in rules.iter().rev() {
out.push(reverse_sub(rule)?);
}
StageItem::Parallel(out)
}
StageItem::Sequential(rules) => {
let mut out = Vec::with_capacity(rules.len());
for rule in rules.iter().rev() {
out.push(reverse_sub(rule)?);
}
StageItem::Sequential(out)
}
StageItem::Run {
dep,
stage,
reverse_run,
} => StageItem::Run {
dep: dep.clone(),
stage: stage.clone(),
reverse_run: toggle(*reverse_run),
},
StageItem::Funcall(funcall) => StageItem::Funcall(reverse_funcall(funcall)?),
})
}
fn reverse_funcall(funcall: &Funcall) -> Result<Funcall> {
let name = funcall.name.reverse_name().ok_or_else(|| {
Error::Unsupported(
"funcall upcase has no reverse (the Ruby reverse_function map omits it)".into(),
)
})?;
Ok(Funcall {
name,
kwargs: funcall.kwargs.clone(),
reverse_run: toggle(funcall.reverse_run),
})
}
pub fn reverse_sub(sub: &Sub) -> Result<Sub> {
let (xfrom, xto) = match &sub.to {
SubTo::Upcase => (item_downcase(&sub.from)?, SubTo::Downcase),
SubTo::Downcase => (item_upcase(&sub.from)?, SubTo::Upcase),
SubTo::Item(to) => {
let (f, t) = reverse_transfer(sub.from.clone(), to.clone())?;
(f, SubTo::Item(t))
}
};
let mut rrun = toggle(sub.reverse_run);
if rrun.is_none()
&& sub.before.is_none()
&& sub.not_before.is_none()
&& sub.after.is_none()
&& sub.not_after.is_none()
&& is_empty_from(&xfrom)
{
rrun = Some(true);
}
let mut reversed = Sub {
before: sub.before.clone(),
not_before: sub.not_before.clone(),
after: sub.after.clone(),
not_after: sub.not_after.clone(),
from: xfrom,
to: xto,
group_count: 0,
reverse_run: rrun,
};
reversed.group_count = assign_group_ids(&mut reversed);
Ok(reversed)
}
fn is_empty_from(item: &Item) -> bool {
match item {
Item::None | Item::Prim(Prim::None_) => true,
Item::Str(s) => s.is_empty(),
_ => false,
}
}
pub fn item_downcase(item: &Item) -> Result<Item> {
item_case(item, false)
}
pub fn item_upcase(item: &Item) -> Result<Item> {
item_case(item, true)
}
fn item_case(item: &Item, up: bool) -> Result<Item> {
let apply = |s: &str| {
if up {
s.to_uppercase()
} else {
s.to_lowercase()
}
};
Ok(match item {
Item::Str(s) => Item::Str(apply(s)),
Item::None => Item::None,
Item::Prim(_) | Item::Alias { .. } | Item::CaptureRef(_) => item.clone(),
Item::CaptureGroup { id, inner } => Item::CaptureGroup {
id: *id,
inner: Box::new(item_case(inner, up)?),
},
Item::Concat(parts) => Item::Concat(
parts
.iter()
.map(|p| item_case(p, up))
.collect::<Result<Vec<_>>>()?,
),
Item::Range(lo, hi) => {
let mut entries = Vec::new();
let mut c = *lo;
loop {
entries.push(Item::Str(apply(&c.to_string())));
if c == *hi {
break;
}
match char::from_u32(c as u32 + 1) {
Some(n) => c = n,
None => break,
}
}
Item::Set(entries)
}
Item::Set(entries) => Item::Set(
entries
.iter()
.map(|e| item_case(e, up))
.collect::<Result<Vec<_>>>()?,
),
Item::Maybe(_) | Item::Some(_) => {
return Err(Error::Unsupported(
"repeat item in a to-upcase/downcase sub (the Ruby reverse raises NoMethodError)"
.into(),
))
}
Item::Func(_) => {
return Err(Error::Unsupported(
"function item in a sub `from` (the Ruby adapter rejects it)".into(),
))
}
})
}
fn boundary_like(item: &Item) -> bool {
matches!(
item,
Item::Prim(Prim::Boundary | Prim::NonWordBoundary | Prim::LineStart | Prim::LineEnd)
)
}
fn group_plus_compact(prefix: Vec<Item>, last: Item) -> Item {
let mut children = prefix;
match last {
Item::Concat(parts) => children.extend(parts),
other => children.push(other),
}
let kept: Vec<Item> = children
.into_iter()
.filter(|c| match c {
Item::None | Item::Prim(Prim::None_) => false,
Item::Str(s) => !s.is_empty(),
_ => true,
})
.collect();
match kept.len() {
0 => Item::None,
1 => kept.into_iter().next().unwrap(),
_ => Item::Concat(kept),
}
}
fn reverse_transfer(from: Item, to: Item) -> Result<(Item, Item)> {
let mut from = from;
let mut to = to;
match &from {
Item::Concat(parts) => {
let first = parts.first().cloned();
let last = parts.last().cloned();
if let Some(f) = &first {
if boundary_like(f) {
to = group_plus_compact(vec![f.clone()], to);
let mut rest = parts.clone();
rest.remove(0);
from = compact_children(rest);
}
}
if let Some(l) = &last {
if boundary_like(l) {
match &from {
Item::Concat(prev) => {
let mut shorter = prev.clone();
shorter.pop();
to = group_plus_compact(vec![to], l.clone());
from = compact_children(shorter);
}
_ => {
return Err(Error::Unsupported(
"boundary hoist consumed the whole from group \
(the Ruby reverse raises NoMethodError)"
.into(),
))
}
}
}
}
}
single => {
if boundary_like(single) {
to = if matches!(single, Item::Prim(Prim::LineEnd)) {
group_plus_compact(vec![to], single.clone())
} else {
group_plus_compact(vec![single.clone()], to)
};
from = Item::Prim(Prim::None_);
}
}
}
let mut state = TransferState::default();
let visited_from = transfer_visit(&from, Side::From, &mut state)?;
let visited_to = transfer_visit(&to, Side::To, &mut state)?;
Ok((visited_to, visited_from))
}
fn compact_children(children: Vec<Item>) -> Item {
let kept: Vec<Item> = children
.into_iter()
.filter(|c| match c {
Item::None | Item::Prim(Prim::None_) => false,
Item::Str(s) => !s.is_empty(),
_ => true,
})
.collect();
match kept.len() {
0 => Item::None,
1 => kept.into_iter().next().unwrap(),
_ => Item::Concat(kept),
}
}
#[derive(Clone, Copy, PartialEq)]
enum Side {
From,
To,
}
#[derive(Default)]
struct TransferState {
#[allow(dead_code)]
left: Vec<Item>,
right: std::collections::BTreeSet<usize>,
}
fn transfer_visit(node: &Item, side: Side, state: &mut TransferState) -> Result<Item> {
Ok(match node {
Item::Alias { name, .. } if name == "kor_maybedash" => {
state.left.push(node.clone());
Item::CaptureRef(state.left.len())
}
Item::Str(_) | Item::None | Item::Prim(_) | Item::Alias { .. } | Item::Range(_, _) => {
node.clone()
}
Item::Set(entries) => Item::Set(
entries
.iter()
.map(|e| transfer_visit(e, side, state))
.collect::<Result<Vec<_>>>()?,
),
Item::Concat(parts) => Item::Concat(
parts
.iter()
.map(|p| transfer_visit(p, side, state))
.collect::<Result<Vec<_>>>()?,
),
Item::Maybe(inner) => Item::Maybe(Box::new(transfer_visit(inner, side, state)?)),
Item::Some(inner) => Item::Some(Box::new(transfer_visit(inner, side, state)?)),
Item::CaptureRef(id) => match side {
Side::From => node.clone(),
Side::To => {
if state.right.contains(id) {
node.clone()
} else {
state.right.insert(*id);
match state.left.get(id.wrapping_sub(1)) {
Some(group) => group.clone(),
None => {
return Err(Error::MapLogic(
"Capture count doesn't match".into(),
))
}
}
}
}
},
Item::CaptureGroup { inner, .. } => {
state.left.push(node.clone());
let out = Item::CaptureRef(state.left.len());
transfer_visit(inner, side, state)?;
out
}
Item::Func(_) => {
return Err(Error::Unsupported(
"Type Item::Func unhandled in reverse_transfer_visit (the Ruby raises \
\"Type ... unhandled!\")"
.into(),
))
}
})
}