From 897e85c2d73576c8fadb442df3b91446dea12a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20H=C3=B6lting?= <87192362+moritz-hoelting@users.noreply.github.com> Date: Fri, 8 Nov 2024 00:32:52 +0100 Subject: [PATCH] add macro string to execute command --- src/datapack/command/execute/conditional.rs | 241 ++++++++++++++------ src/datapack/command/execute/mod.rs | 156 ++++++++++--- src/datapack/command/mod.rs | 75 ++++-- src/datapack/function.rs | 12 +- src/util/macro_string.rs | 45 +++- 5 files changed, 400 insertions(+), 129 deletions(-) diff --git a/src/datapack/command/execute/conditional.rs b/src/datapack/command/execute/conditional.rs index 194cd15..9268b86 100644 --- a/src/datapack/command/execute/conditional.rs +++ b/src/datapack/command/execute/conditional.rs @@ -1,9 +1,15 @@ use chksum_md5 as md5; -use std::ops::{BitAnd, BitOr, Not}; +use std::{ + collections::HashSet, + ops::{BitAnd, BitOr, Not}, +}; use crate::{ prelude::Command, - util::compile::{CompileOptions, FunctionCompilerState, MutCompilerState}, + util::{ + compile::{CompileOptions, FunctionCompilerState, MutCompilerState}, + MacroString, + }, }; use super::Execute; @@ -80,10 +86,18 @@ fn compile_pre_20_format( .into(), ); } - Command::Group(group_cmd) + let group = Command::Group(group_cmd); + let allows_prefix = !group.forbid_prefix(); + group .compile(options, global_state, function_state) .iter() - .map(|s| (true, "run ".to_string() + s)) + .map(|s| { + if allows_prefix { + (true, "run ".to_string() + s) + } else { + (false, s.clone()) + } + }) .collect() } else { then.compile_internal(String::new(), false, options, global_state, function_state) @@ -111,11 +125,10 @@ fn compile_pre_20_format( tracing::error!("No success_uid found for each_or_cmd, using default"); "if_success" }); - Condition::Atom(format!("data storage shulkerbox:cond {{{success_uid}:1b}}")).compile( - options, - global_state, - function_state, - ) + Condition::Atom(MacroString::from(format!( + "data storage shulkerbox:cond {{{success_uid}:1b}}" + ))) + .compile(options, global_state, function_state) } else { str_cond }; @@ -128,9 +141,10 @@ fn compile_pre_20_format( tracing::error!("No success_uid found for each_or_cmd, using default"); "if_success" }); - let else_cond = - (!Condition::Atom(format!("data storage shulkerbox:cond {{{success_uid}:1b}}"))) - .compile(options, global_state, function_state); + let else_cond = (!Condition::Atom(MacroString::from(format!( + "data storage shulkerbox:cond {{{success_uid}:1b}}" + )))) + .compile(options, global_state, function_state); let el = el.compile_internal( String::new(), else_cond.len() > 1, @@ -186,7 +200,9 @@ fn compile_since_20_format( ) -> Vec<(bool, String)> { let then_count = then.get_count(options); - let str_cond = cond.clone().compile(options, global_state, function_state); + let str_cond = cond + .clone() + .compile_keep_macros(options, global_state, function_state); // if the conditions have multiple parts joined by a disjunction or an else part, commands need to be grouped if el.is_some() || str_cond.len() > 1 { @@ -199,10 +215,12 @@ fn compile_since_20_format( global_state, function_state, ); - Command::Group(group_cmds) + let group = Command::Group(group_cmds); + let allows_prefix = !group.forbid_prefix(); + group .compile(options, global_state, function_state) .into_iter() - .map(|s| (true, s)) + .map(|s| (allows_prefix, s)) .collect() } else if then_count > 1 { let then_cmd = match then.clone() { @@ -210,36 +228,48 @@ fn compile_since_20_format( Execute::Runs(cmds) => cmds, ex => vec![Command::Execute(ex)], }; - let then_cmd_str = Command::Group(then_cmd) - .compile(options, global_state, function_state) + let group_cmd = Command::Group(then_cmd); + let then_cmd = if group_cmd.forbid_prefix() { + group_cmd + } else { + Command::Concat( + Box::new(Command::Raw("run ".to_string())), + Box::new(group_cmd), + ) + }; + combine_conditions_commands_concat(str_cond, &then_cmd) .into_iter() - .map(|s| (true, format!("run {s}"))) - .collect::>(); - combine_conditions_commands(str_cond, &then_cmd_str) - .into_iter() - .map(|(use_prefix, cmd)| { - let cmd = if use_prefix { - prefix.to_string() + &cmd - } else { - cmd - }; - (use_prefix, cmd) + .map(|cmd| { + ( + cmd.forbid_prefix(), + cmd.compile(options, global_state, function_state), + ) + }) + .flat_map(|(forbid_prefix, cmds)| { + cmds.into_iter() + .map(move |cmd| (!forbid_prefix, prefix.to_string() + &cmd)) }) .collect() } else { - let str_cmd = - then.compile_internal(String::new(), false, options, global_state, function_state); - combine_conditions_commands(str_cond, &str_cmd) - .into_iter() - .map(|(use_prefix, cmd)| { - let cmd = if use_prefix { - prefix.to_string() + &cmd - } else { - cmd - }; - (use_prefix, cmd) - }) - .collect() + combine_conditions_commands_concat( + str_cond, + &Command::Concat( + Box::new(Command::Raw("run ".to_string())), + Box::new(Command::Execute(then.clone())), + ), + ) + .into_iter() + .map(|cmd| { + ( + cmd.forbid_prefix(), + cmd.compile(options, global_state, function_state), + ) + }) + .flat_map(|(forbid_prefix, cmds)| { + cmds.into_iter() + .map(move |cmd| (!forbid_prefix, prefix.to_string() + &cmd)) + }) + .collect() } } @@ -263,8 +293,29 @@ fn combine_conditions_commands( .collect() } +fn combine_conditions_commands_concat( + conditions: Vec, + command: &Command, +) -> Vec { + if command.forbid_prefix() { + vec![command.clone()] + } else { + conditions + .into_iter() + .map(|cond| { + let prefix = if cond.contains_macro() { + Command::UsesMacro(cond + " ") + } else { + Command::Raw(cond.compile() + " ") + }; + Command::Concat(Box::new(prefix), Box::new(command.clone())) + }) + .collect() + } +} + fn handle_return_group_case_since_20( - str_cond: Vec, + str_cond: Vec, then: &Execute, el: Option<&Execute>, prefix: &str, @@ -278,15 +329,28 @@ fn handle_return_group_case_since_20( Execute::Runs(cmds) => cmds, ex => vec![Command::Execute(ex)], }; - let then_cmd_str = Command::Group(then_cmd) - .compile(options, global_state, function_state) + let group = Command::Group(then_cmd); + let then_cmd_concat = if group.forbid_prefix() { + group + } else { + Command::Concat( + Box::new(Command::Raw("run return run ".to_string())), + Box::new(group), + ) + }; + let then_cond_concat = combine_conditions_commands_concat(str_cond, &then_cmd_concat); + let mut group_cmds = then_cond_concat .into_iter() - .map(|s| (true, format!("run return run {s}"))) - .collect::>(); - let then_cond_str = combine_conditions_commands(str_cond, &then_cmd_str); - let mut group_cmds = then_cond_str - .into_iter() - .map(|(_, cmd)| Command::Raw(format!("execute {cmd}"))) + .map(|cmd| { + if cmd.forbid_prefix() { + cmd + } else { + Command::Concat( + Box::new(Command::Raw("execute ".to_string())), + Box::new(cmd), + ) + } + }) .collect::>(); if let Some(el) = el { handle_else_since_20( @@ -311,7 +375,7 @@ fn handle_else_since_20( ) { let el_cmd = match el { Execute::If(cond, then, el) => handle_return_group_case_since_20( - cond.compile(options, global_state, function_state), + cond.compile_keep_macros(options, global_state, function_state), &then, el.as_deref(), prefix, @@ -321,7 +385,7 @@ fn handle_else_since_20( ), Execute::Run(cmd) => match *cmd { Command::Execute(Execute::If(cond, then, el)) => handle_return_group_case_since_20( - cond.compile(options, global_state, function_state), + cond.compile_keep_macros(options, global_state, function_state), &then, el.as_deref(), prefix, @@ -343,7 +407,7 @@ fn handle_else_since_20( #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Condition { - Atom(String), + Atom(MacroString), Not(Box), And(Box, Box), Or(Box, Box), @@ -392,21 +456,21 @@ impl Condition { } } - /// Convert the condition into a string. + /// Convert the condition into a [`MacroString`]. /// /// Will fail if the condition contains an `Or` variant. Use `compile` instead. - fn str_cond(&self) -> Option { + fn str_cond(&self) -> Option { match self { - Self::Atom(s) => Some("if ".to_string() + s), + Self::Atom(s) => Some(MacroString::from("if ") + s.clone()), Self::Not(n) => match *(*n).clone() { - Self::Atom(s) => Some("unless ".to_string() + &s), + Self::Atom(s) => Some(MacroString::from("unless ") + s), _ => None, }, Self::And(a, b) => { let a = a.str_cond()?; let b = b.str_cond()?; - Some(a + " " + &b) + Some(a + MacroString::from(" ") + b) } Self::Or(..) => None, } @@ -414,11 +478,24 @@ impl Condition { /// Compile the condition into a list of strings that can be used in Minecraft. pub fn compile( + &self, + options: &CompileOptions, + global_state: &MutCompilerState, + function_state: &FunctionCompilerState, + ) -> Vec { + self.compile_keep_macros(options, global_state, function_state) + .into_iter() + .map(|s| s.compile()) + .collect() + } + + /// Compile the condition into a list of macro strings. + pub fn compile_keep_macros( &self, _options: &CompileOptions, _global_state: &MutCompilerState, _function_state: &FunctionCompilerState, - ) -> Vec { + ) -> Vec { let truth_table = self.to_truth_table(); truth_table @@ -427,13 +504,37 @@ impl Condition { c.str_cond() .expect("Truth table should not contain Or variants") }) - .collect() + .collect::>() + } + + /// Check whether the condition contains a macro. + #[must_use] + pub fn contains_macro(&self) -> bool { + match self { + Self::Atom(s) => s.contains_macro(), + Self::Not(n) => n.contains_macro(), + Self::And(a, b) | Self::Or(a, b) => a.contains_macro() || b.contains_macro(), + } + } + + /// Returns the names of the macros used + #[must_use] + pub fn get_macros(&self) -> HashSet<&str> { + match self { + Self::Atom(s) => s.get_macros(), + Self::Not(n) => n.get_macros(), + Self::And(a, b) | Self::Or(a, b) => { + let mut set = a.get_macros(); + set.extend(b.get_macros()); + set + } + } } } impl From<&str> for Condition { fn from(s: &str) -> Self { - Self::Atom(s.to_string()) + Self::Atom(s.into()) } } @@ -459,12 +560,6 @@ impl BitOr for Condition { } } -impl From for Command { - fn from(ex: Execute) -> Self { - Self::Execute(ex) - } -} - #[cfg(test)] mod tests { use super::*; @@ -473,8 +568,8 @@ mod tests { #[test] fn test_condition() { let c1 = Condition::from("foo"); - let c2 = Condition::Atom("bar".to_string()); - let c3 = Condition::Atom("baz".to_string()); + let c2 = Condition::Atom("bar".into()); + let c3 = Condition::Atom("baz".into()); assert_eq!( (c1.clone() & c2.clone()).normalize(), @@ -517,10 +612,10 @@ mod tests { #[allow(clippy::redundant_clone)] #[test] fn test_truth_table() { - let c1 = Condition::Atom("foo".to_string()); - let c2 = Condition::Atom("bar".to_string()); - let c3 = Condition::Atom("baz".to_string()); - let c4 = Condition::Atom("foobar".to_string()); + let c1 = Condition::Atom("foo".into()); + let c2 = Condition::Atom("bar".into()); + let c3 = Condition::Atom("baz".into()); + let c4 = Condition::Atom("foobar".into()); assert_eq!( (c1.clone() & c2.clone()).to_truth_table(), diff --git a/src/datapack/command/execute/mod.rs b/src/datapack/command/execute/mod.rs index 8b23f5b..fa6e94b 100644 --- a/src/datapack/command/execute/mod.rs +++ b/src/datapack/command/execute/mod.rs @@ -1,9 +1,9 @@ -use std::ops::RangeInclusive; +use std::{collections::HashSet, ops::RangeInclusive}; use super::Command; use crate::util::{ compile::{CompileOptions, FunctionCompilerState, MutCompilerState}, - ExtendableQueue, + ExtendableQueue, MacroString, }; mod conditional; @@ -15,18 +15,18 @@ pub use conditional::Condition; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Execute { - Align(String, Box), - Anchored(String, Box), - As(String, Box), - At(String, Box), - AsAt(String, Box), - Facing(String, Box), - In(String, Box), - On(String, Box), - Positioned(String, Box), - Rotated(String, Box), - Store(String, Box), - Summon(String, Box), + Align(MacroString, Box), + Anchored(MacroString, Box), + As(MacroString, Box), + At(MacroString, Box), + AsAt(MacroString, Box), + Facing(MacroString, Box), + In(MacroString, Box), + On(MacroString, Box), + Positioned(MacroString, Box), + Rotated(MacroString, Box), + Store(MacroString, Box), + Summon(MacroString, Box), If(Condition, Box, Option>), Run(Box), Runs(Vec), @@ -78,15 +78,23 @@ impl Execute { | Self::On(arg, next) | Self::Positioned(arg, next) | Self::Rotated(arg, next) - | Self::Store(arg, next) => next.compile_internal( - format!("{prefix}{op} {arg} ", op = self.variant_name()), + | Self::Store(arg, next) + | Self::Summon(arg, next) => next.compile_internal( + format!( + "{prefix}{op} {arg} ", + op = self.variant_name(), + arg = arg.compile() + ), require_grouping, options, global_state, function_state, ), Self::AsAt(selector, next) => next.compile_internal( - format!("{prefix}as {selector} at @s "), + format!( + "{prefix}as {selector} at @s ", + selector = selector.compile() + ), require_grouping, options, global_state, @@ -101,13 +109,6 @@ impl Execute { global_state, function_state, ), - Self::Summon(arg, next) => next.compile_internal( - format!("{prefix}{op} {arg} ", op = self.variant_name()), - true, - options, - global_state, - function_state, - ), Self::Run(command) => match &**command { Command::Execute(ex) => ex.compile_internal( prefix, @@ -119,19 +120,27 @@ impl Execute { command => command .compile(options, global_state, function_state) .into_iter() - .map(|c| map_run_cmd(c, &prefix)) + .map(|c| map_run_cmd(command.forbid_prefix(), c, &prefix)) .collect(), }, Self::Runs(commands) if !require_grouping => commands .iter() - .flat_map(|c| c.compile(options, global_state, function_state)) - .map(|c| map_run_cmd(c, &prefix)) - .collect(), - Self::Runs(commands) => Command::Group(commands.clone()) - .compile(options, global_state, function_state) - .into_iter() - .map(|c| map_run_cmd(c, &prefix)) + .flat_map(|c| { + let forbid_prefix = c.forbid_prefix(); + c.compile(options, global_state, function_state) + .into_iter() + .map(move |c| (forbid_prefix, c)) + }) + .map(|(forbid_prefix, c)| map_run_cmd(forbid_prefix, c, &prefix)) .collect(), + Self::Runs(commands) => { + let group = Command::Group(commands.clone()); + group + .compile(options, global_state, function_state) + .into_iter() + .map(|c| map_run_cmd(group.forbid_prefix(), c, &prefix)) + .collect() + } } } @@ -200,12 +209,87 @@ impl Execute { } } } + + /// Check whether the execute command contains a macro. + #[must_use] + pub fn contains_macro(&self) -> bool { + match self { + Self::Facing(s, next) + | Self::Store(s, next) + | Self::Positioned(s, next) + | Self::Rotated(s, next) + | Self::In(s, next) + | Self::As(s, next) + | Self::At(s, next) + | Self::AsAt(s, next) + | Self::Align(s, next) + | Self::Anchored(s, next) + | Self::Summon(s, next) + | Self::On(s, next) => s.contains_macro() || next.contains_macro(), + Self::If(cond, then, el) => { + cond.contains_macro() + || then.contains_macro() + || el.as_deref().map_or(false, Self::contains_macro) + } + Self::Run(cmd) => cmd.contains_macro(), + Self::Runs(cmds) => cmds.iter().any(super::Command::contains_macro), + } + } + + /// Returns the names of the macros used + #[must_use] + pub fn get_macros(&self) -> HashSet<&str> { + match self { + Self::Facing(s, _) + | Self::Store(s, _) + | Self::Positioned(s, _) + | Self::Rotated(s, _) + | Self::In(s, _) + | Self::As(s, _) + | Self::At(s, _) + | Self::AsAt(s, _) + | Self::Align(s, _) + | Self::Anchored(s, _) + | Self::Summon(s, _) + | Self::On(s, _) => s.get_macros(), + Self::If(cond, then, el) => { + let mut macros = cond.get_macros(); + macros.extend(then.get_macros()); + if let Some(el) = el { + macros.extend(el.get_macros()); + } + macros + } + Self::Run(cmd) => cmd.get_macros(), + Self::Runs(cmds) => cmds.iter().flat_map(|cmd| cmd.get_macros()).collect(), + } + } +} + +impl From for Command { + fn from(execute: Execute) -> Self { + Self::Execute(execute) + } +} + +impl From for Execute { + fn from(command: Command) -> Self { + Self::Run(Box::new(command)) + } +} +impl From for Execute +where + V: Into>, +{ + fn from(value: V) -> Self { + Self::Runs(value.into()) + } } /// Combine command parts, respecting if the second part is a comment /// The first tuple element is a boolean indicating if the prefix should be used -fn map_run_cmd(cmd: String, prefix: &str) -> (bool, String) { - if cmd.starts_with('#') || cmd.is_empty() || cmd.chars().all(char::is_whitespace) { +fn map_run_cmd(forbid_prefix: bool, cmd: String, prefix: &str) -> (bool, String) { + if forbid_prefix || cmd.is_empty() || cmd.chars().all(char::is_whitespace) { (false, cmd) } else { (true, prefix.to_string() + "run " + &cmd) @@ -219,7 +303,7 @@ mod tests { #[test] fn test_compile() { let compiled = Execute::As( - "@ְa".to_string(), + "@a".into(), Box::new(Execute::If( "block ~ ~-1 ~ minecraft:stone".into(), Box::new(Execute::Run(Box::new("say hi".into()))), @@ -234,7 +318,7 @@ mod tests { assert_eq!( compiled, - vec!["execute as @ְa if block ~ ~-1 ~ minecraft:stone run say hi".to_string()] + vec!["execute as @a if block ~ ~-1 ~ minecraft:stone run say hi".to_string()] ); let direct = Execute::Run(Box::new("say direct".into())).compile( diff --git a/src/datapack/command/mod.rs b/src/datapack/command/mod.rs index 15ee7ac..99defe1 100644 --- a/src/datapack/command/mod.rs +++ b/src/datapack/command/mod.rs @@ -36,6 +36,8 @@ pub enum Command { Group(Vec), /// Comment to be added to the function Comment(String), + /// Command that is a concatenation of two commands + Concat(Box, Box), } impl Command { @@ -59,6 +61,23 @@ impl Command { Self::Execute(ex) => ex.compile(options, global_state, function_state), Self::Group(commands) => compile_group(commands, options, global_state, function_state), Self::Comment(comment) => vec!["#".to_string() + comment], + Self::Concat(a, b) => { + let a = a.compile(options, global_state, function_state); + let b = b.compile(options, global_state, function_state); + a.into_iter() + .flat_map(|a| { + b.iter().map(move |b| { + if a.is_empty() { + b.clone() + } else if b.is_empty() { + a.clone() + } else { + a.clone() + b + } + }) + }) + .collect() + } } } @@ -73,27 +92,36 @@ impl Command { Self::UsesMacro(cmd) => cmd.line_count(), Self::Execute(ex) => ex.get_count(options), Self::Group(_) => 1, + Self::Concat(a, b) => a.get_count(options) + b.get_count(options) - 1, } } /// Check whether the command is valid with the given pack format. #[must_use] pub fn validate(&self, pack_formats: &RangeInclusive) -> bool { - match self { + let command_valid = match self { Self::Comment(_) | Self::Debug(_) | Self::Group(_) => true, Self::Raw(cmd) => validate_raw_cmd(cmd, pack_formats), Self::UsesMacro(cmd) => validate_raw_cmd(&cmd.compile(), pack_formats), Self::Execute(ex) => ex.validate(pack_formats), + Self::Concat(a, b) => a.validate(pack_formats) && b.validate(pack_formats), + }; + if pack_formats.start() < &16 { + command_valid && !self.contains_macro() + } else { + command_valid } } /// Check whether the command contains a macro. #[must_use] - pub fn contains_macro(&self, options: &CompileOptions) -> bool { + pub fn contains_macro(&self) -> bool { match self { - Self::Raw(_) | Self::Comment(_) | Self::Execute(_) => false, + Self::Raw(_) | Self::Comment(_) => false, Self::UsesMacro(s) | Self::Debug(s) => s.contains_macro(), - Self::Group(commands) => group_contains_macro(commands, options), + Self::Group(commands) => group_contains_macro(commands), + Self::Execute(ex) => ex.contains_macro(), + Self::Concat(a, b) => a.contains_macro() || b.contains_macro(), } } @@ -101,9 +129,26 @@ impl Command { #[must_use] pub fn get_macros(&self) -> HashSet<&str> { match self { - Self::Raw(_) | Self::Comment(_) | Self::Execute(_) => HashSet::new(), + Self::Raw(_) | Self::Comment(_) => HashSet::new(), Self::UsesMacro(s) | Self::Debug(s) => s.get_macros(), Self::Group(commands) => group_get_macros(commands), + Self::Execute(ex) => ex.get_macros(), + Self::Concat(a, b) => { + let mut macros = a.get_macros(); + macros.extend(b.get_macros()); + macros + } + } + } + + /// Check whether the command should not have a prefix. + #[must_use] + pub fn forbid_prefix(&self) -> bool { + match self { + Self::Comment(_) => true, + Self::Raw(_) | Self::Debug(_) | Self::Execute(_) | Self::UsesMacro(_) => false, + Self::Group(commands) => commands.len() == 1 && commands[0].forbid_prefix(), + Self::Concat(a, _) => a.forbid_prefix(), } } } @@ -149,7 +194,7 @@ fn compile_group( // only create a function if there are more than one command if command_count > 1 { let uid = function_state.request_uid(); - let pass_macros = group_contains_macro(commands, options); + let pass_macros = group_contains_macro(commands); // calculate a hashed path for the function in the `sb` subfolder let function_path = { @@ -172,15 +217,11 @@ fn compile_group( let mut function_invocation = format!("function {namespace}:{function_path}"); if pass_macros { - let macros_block = - group_get_macros(commands) - .into_iter() - .fold(String::new(), |mut s, m| { - use std::fmt::Write; - - write!(&mut s, "{m}:$({m})").expect("can always write to string"); - s - }); + let macros_block = group_get_macros(commands) + .into_iter() + .map(|m| format!("{m}:$({m})")) + .collect::>() + .join(","); function_invocation.push_str(&format!(" {{{macros_block}}}")); } @@ -193,8 +234,8 @@ fn compile_group( } } -fn group_contains_macro(commands: &[Command], options: &CompileOptions) -> bool { - commands.iter().any(|cmd| cmd.contains_macro(options)) +fn group_contains_macro(commands: &[Command]) -> bool { + commands.iter().any(Command::contains_macro) } fn group_get_macros(commands: &[Command]) -> HashSet<&str> { diff --git a/src/datapack/function.rs b/src/datapack/function.rs index 80ca510..cce8c4c 100644 --- a/src/datapack/function.rs +++ b/src/datapack/function.rs @@ -65,8 +65,16 @@ impl Function { .flat_map(|c| { let cmds = c.compile(options, global_state, function_state); - if c.contains_macro(options) { - cmds.into_iter().map(|c| format!("${c}")).collect() + if c.contains_macro() { + cmds.into_iter() + .map(|c| { + if c.starts_with('#') { + c + } else { + format!("${c}") + } + }) + .collect() } else { cmds } diff --git a/src/util/macro_string.rs b/src/util/macro_string.rs index e1d9b27..11dfbc4 100644 --- a/src/util/macro_string.rs +++ b/src/util/macro_string.rs @@ -1,6 +1,6 @@ #![allow(clippy::module_name_repetitions)] -use std::{borrow::Cow, collections::HashSet}; +use std::{borrow::Cow, collections::HashSet, ops::Add}; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -102,3 +102,46 @@ impl From<&str> for MacroStringPart { Self::String(value.to_string()) } } + +impl Add for MacroString { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (Self::String(mut s1), Self::String(s2)) => { + s1.push_str(&s2); + Self::String(s1) + } + (Self::String(s1), Self::MacroString(s2)) => Self::MacroString( + std::iter::once(MacroStringPart::String(s1)) + .chain(s2) + .collect(), + ), + (Self::MacroString(mut s1), Self::String(s2)) => { + s1.push(MacroStringPart::String(s2)); + Self::MacroString(s1) + } + (Self::MacroString(mut s1), Self::MacroString(s2)) => { + s1.extend(s2); + Self::MacroString(s1) + } + } + } +} + +impl Add<&str> for MacroString { + type Output = Self; + + fn add(self, rhs: &str) -> Self::Output { + match self { + Self::String(mut s1) => { + s1.push_str(rhs); + Self::String(s1) + } + Self::MacroString(mut s1) => { + s1.push(MacroStringPart::String(rhs.to_string())); + Self::MacroString(s1) + } + } + } +}