├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "pipeop" 7 | version = "0.2.0" 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pipeop" 3 | version = "0.2.0" 4 | edition = "2021" 5 | authors = ["David <75451291+david-d-h@users.noreply.github.com>"] 6 | description = "Adding the pipe operator to Rust with a declarative macro." 7 | license = "MIT" 8 | repository = "https://github.com/david-d-h/pipeop" 9 | homepage = "https://github.com/david-d-h/pipeop" 10 | keywords = ["macro"] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 David den Haan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The pipe operator in Rust 2 | 3 | This crate is exactly what you expect it to be, the pipe operator in Rust. 4 | 5 | ## Usage 6 | 7 | You can construct a "pipeline" by passing any expression and at least a 8 | single pipe into the `::pipeop::pipe!` macro. There are some special things 9 | you can do but, in its most basic form the macro tries to literally call 10 | your pipe with the item in the pipeline. 11 | 12 | ```rust 13 | const fn add_one(to: i32) -> i32 { 14 | to + 1 15 | } 16 | 17 | let result = pipe!(1 |> add_one |> add_one); 18 | assert!(result, 3); 19 | ``` 20 | 21 | ### Invoking methods on the item in the pipeline 22 | 23 | You can invoke methods on the item in the pipeline at any time by 24 | prefixing the pipe with a `.`. 25 | 26 | This example calls the `add` method on the item in the pipelines 27 | with `1` as the single argument. 28 | 29 | ```rust 30 | use std::ops::Add; 31 | pipe!(1 |> .add(1)); 32 | ``` 33 | 34 | ### Closure based pipes 35 | 36 | You can also use closures as pipes, so you don't have to define a 37 | whole new function for every simple operation. Both types of closures are valid, you can have a closure that just 38 | evaluates an expression, or you can have a whole block of code. 39 | 40 | ```rust 41 | pipe!("Hello!" 42 | |> .to_uppercase() 43 | |> |item| println!("{}", item) 44 | ); 45 | ``` 46 | 47 | You can also make closure based pipes look a little nicer 48 | by using this syntax instead. 49 | 50 | ```rust 51 | pipe!("Hello!" 52 | |> .to_uppercase() 53 | |> item in println!("{}", item) 54 | ); 55 | ``` 56 | 57 | You can of course accept a pattern in this "weird closure". 58 | This example extracts the inner `bool` value from the 59 | `Test` instance in the pipeline with pattern matching. 60 | 61 | ```rust 62 | struct Test(bool); 63 | let result = pipe!(Test(true) |> Test(it) in it); 64 | assert_eq!(result, true); 65 | ``` 66 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! pipe { 3 | (@accumulate_individual_pipes [$($callback:tt)*] [] |> $($pipes:tt)+) => ($crate::pipe!( 4 | @accumulate_individual_pipes [$($callback)*] [] [] $($pipes)+ 5 | )); 6 | 7 | (@accumulate_individual_pipes [$($callback:tt)*] [$([ $($pipe:tt)* ])*] [$($buffer:tt)*] |> $($pipes:tt)+) => ($crate::pipe!( 8 | @accumulate_individual_pipes [$($callback)*] [$([ $($pipe)* ])* [$($buffer)*]] [] $($pipes)+ 9 | )); 10 | 11 | (@accumulate_individual_pipes [$($callback:tt)*] [$($pipes:tt)*] [$($previous:tt)*] |>) => (::std::compile_error!( 12 | "expected pipe after pipe operator \"|>\"" 13 | )); 14 | 15 | (@accumulate_individual_pipes [$($callback:tt)*] [$([ $($pipe:tt)* ])*] [$($buffer:tt)*] $tt:tt $($tail:tt)*) => ($crate::pipe!( 16 | @accumulate_individual_pipes [$($callback)*] [$([ $($pipe)* ])*] [$($buffer)* $tt] $($tail)* 17 | )); 18 | 19 | (@accumulate_individual_pipes [$($callback:tt)*] [$([ $($pipe:tt)* ])*] [$($buffer:tt)*]) => ($crate::pipe!( 20 | $($callback)* [$([ $($pipe)* ])* [$($buffer)*]] 21 | )); 22 | 23 | (@accumulate_individual_pipes [$($callback:tt)*] $($pipes:tt)+) => ($crate::pipe!( 24 | @accumulate_individual_pipes [$($callback)*] [] $($pipes)+ 25 | )); 26 | 27 | (@accumulated_expr [$expr:expr] $($pipes:tt)+) => ($crate::pipe!( 28 | @accumulate_individual_pipes [@make_pipeline [$expr]] $($pipes)* 29 | )); 30 | 31 | (@accumulate_expression [$($callback:tt)*] [$($buffer:tt)*] $tt:tt |> $($pipes:tt)+) => ($crate::pipe!( 32 | @finish_expression [$($callback)*] [$($buffer)* $tt] |> $($pipes)+ 33 | )); 34 | 35 | (@accumulate_expression [$($callback:tt)*] [$($buffer:tt)*] $tt:tt $($tail:tt)*) => ($crate::pipe!( 36 | @accumulate_expression [$($callback)*] [$($buffer)* $tt] $($tail)* 37 | )); 38 | 39 | (@accumulate_expression [$($callback:tt)*] [$($buffer:tt)*]) => (::std::compile_error!( 40 | "expected at least one pipe after the pipeline item" 41 | )); 42 | 43 | (@accumulate_expression [$($callback:tt)*] $($tokens:tt)+) => ($crate::pipe!( 44 | @accumulate_expression [$($callback)*] [] $($tokens)+ 45 | )); 46 | 47 | (@finish_expression [$($callback:tt)*] [$expr:expr] $($pipes:tt)+) => ($crate::pipe!( 48 | $($callback)* [$expr] $($pipes)* 49 | )); 50 | 51 | (@finish_expression [$($callback:tt)*] [] $($residual:tt)*) => (::std::compile_error!( 52 | "tried to accumulate an expression for use in the pipeline but found none" 53 | )); 54 | 55 | (@finish_expression [$($callback:tt)*] [$($false_expr:tt)*] $($residual:tt)*) => (::std::compile_error!( 56 | ::std::concat!("could not accumulate an expression for use in the pipeline, found: ", ::std::stringify!($($false_expr)*)) 57 | )); 58 | 59 | (@make_pipeline [$expr:expr] [$([ $($pipe:tt)+ ])+]) => ({ 60 | let current = $expr; 61 | $( 62 | macro_rules! __pipeop_expand_to_current {() => (current)} 63 | let current = $crate::pipe!(@transform_pipe $($pipe)+); 64 | )+ 65 | current 66 | }); 67 | 68 | (@make_pipeline $($tokens:tt)+) => ($crate::pipe!( 69 | @accumulate_expression [@accumulated_expr] $($tokens)* 70 | )); 71 | 72 | (@transform_pipe $(<$ty:ty>)? . $($method:tt)+) => ($crate::pipe!( 73 | @maybe_ends_with_try [@transform_pipe] [] |item $(: $ty)?| item.$($method)+ 74 | )); 75 | 76 | (@transform_pipe [$(try $(@$($_:tt)* $has_try:tt)?)?] $($pipe:tt)+) => ($crate::pipe!( 77 | @finalize_pipe [ $($($has_try)? [try])? ] $($pipe)+ 78 | )); 79 | 80 | (@transform_pipe $pat:pat in $expr:expr) => ($crate::pipe!(@finalize_pipe [] |$pat| $expr)); 81 | 82 | (@transform_pipe | $($closure:tt)+) => ($crate::pipe!(@finalize_pipe [] |$($closure)+)); 83 | 84 | (@transform_pipe $expr:expr) => ($crate::pipe!(@finalize_pipe [] |item| $expr(item))); 85 | 86 | (@transform_pipe $($pipe:tt)*) => (::std::compile_error!( 87 | ::std::concat!("unknown pipe syntax: ", ::std::stringify!($($pipe)*)) 88 | )); 89 | 90 | (@maybe_ends_with_try [$($callback:tt)*] [$($buffer:tt)*] ?) => ($crate::pipe!($($callback)* [try] $($buffer)*)); 91 | 92 | (@maybe_ends_with_try [$($callback:tt)*] [$($buffer:tt)*]) => ($crate::pipe!($($callback)* [] $($buffer)*)); 93 | 94 | (@maybe_ends_with_try [$($callback:tt)*] [$($buffer:tt)*] $tt:tt $($tail:tt)*) => ($crate::pipe!( 95 | @maybe_ends_with_try [$($callback)*] [$($buffer)* $tt] $($tail)* 96 | )); 97 | 98 | (@finalize_pipe [$([ $modifier:tt ])*] $($pipe:tt)+) => ($crate::pipe!( 99 | @apply_modifiers [$([ $modifier ])*] [$crate::call_with($($pipe)+, __pipeop_expand_to_current!())] 100 | )); 101 | 102 | (@apply_modifiers [[try] $($modifiers:tt)*] [$($pipe:tt)*]) => ($crate::pipe!(@apply_modifiers [$($modifiers)*] [$($pipe)*?])); 103 | 104 | (@apply_modifiers [[$($unknown:tt)+] $($_:tt)*]) => (::std::compile_error!( 105 | ::std::concat!("unknown modifier: ", ::std::stringify!($($unknown)*)) 106 | )); 107 | 108 | (@apply_modifiers [] [$($pipe:tt)*]) => ($($pipe)*); 109 | 110 | ($($tokens:tt)+) => ($crate::pipe!( 111 | @make_pipeline $($tokens)+ 112 | )); 113 | 114 | // An empty pipeline results in a unit type. 115 | () => (()); 116 | } 117 | 118 | pub fn call_with R>(f: F, t: T) -> R { 119 | f(t) 120 | } 121 | 122 | #[cfg(test)] 123 | mod tests { 124 | use std::num::ParseIntError; 125 | use std::ops::Add; 126 | 127 | #[test] 128 | fn fn_pipe() { 129 | const fn add_one(to: i32) -> i32 { 130 | to + 1 131 | } 132 | let result = pipe!(1 |> add_one); 133 | assert_eq!(result, 2); 134 | } 135 | 136 | #[test] 137 | fn closure_pipe() { 138 | let result = pipe!(1 |> |item| item + 1); 139 | assert_eq!(result, 2); 140 | } 141 | 142 | #[test] 143 | fn method_invocation_pipe() { 144 | let result = pipe!(1 |> .add(1)); 145 | assert_eq!(result, 2); 146 | } 147 | 148 | #[test] 149 | fn explicit_type_expectation_in_method_invocation() { 150 | let result = pipe!(1 |> .add(2) |> .add(1)); 151 | assert_eq!(result, 4); 152 | } 153 | 154 | #[test] 155 | fn many_pipes() { 156 | let result = pipe!(1i32 157 | |> .add(1) 158 | |> |item| item + 1 159 | |> .add(1) 160 | ); 161 | 162 | assert_eq!(result, 4); 163 | } 164 | 165 | #[test] 166 | fn can_use_try_modifier() -> Result<(), ParseIntError> { 167 | let result = pipe!("1" |> .parse::()?); 168 | assert_eq!(result, 1u8); 169 | Ok(()) 170 | } 171 | 172 | #[test] 173 | fn partial_invocation() { 174 | let additive = 2; 175 | let result = pipe!(1 |> item in Add::add(item, additive)); 176 | assert_eq!(result, 3); 177 | } 178 | 179 | #[test] 180 | fn pat_in_partial_invocation() { 181 | struct Test(bool); 182 | let result = pipe!(Test(true) |> Test(it) in it); 183 | assert_eq!(result, true); 184 | } 185 | } 186 | --------------------------------------------------------------------------------