diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index b9cf34a..40a0007 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -34,6 +34,7 @@ jobs: uses: actions-rs/tarpaulin@v0.1 with: args: '--all-features --run-types Doctests,Tests' + version: '0.15.0' timeout: 120 - name: Upload to codecov.io diff --git a/Cargo.toml b/Cargo.toml index 7bb2fed..af7fb7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruex" -version = "0.1.3" +version = "0.1.5" authors = ["Victor Dudochkin "] readme = "README.md" homepage = "https://angular-rust.github.io/ruex" @@ -18,4 +18,22 @@ maintenance = { status = "actively-developed" } [dependencies] log = "0.4" serde = { version = "1.0", features = ["derive"] } -once_cell = "1.7.2" +once_cell = "1.7" +futures = "0.3" +ruex-macro = { version="0", path = "macro" } +colored = "2.0" +atty = "0.2" +url = "2.2" +urlencoding = "2.1" +color-backtrace = "0.5" +fern = { version = "0.6", features = ["colored", "syslog-6", "meta-logging-in-format", "chrono"] } +humantime = "2.1" +syslog = "6" +# contracts = "0.6" +# systemd-journal-logger = "0.7" +rust-companion = "0" +quanta = "0.11.0" +perf-event = "0.4.8" + +[build-dependencies] +rust-companion = "0" diff --git a/README.md b/README.md index 1756acb..af5d82b 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ [license-url]: https://github.com/angular-rust/ruex/blob/master/LICENSE [gitter-badge]: https://img.shields.io/gitter/room/angular_rust/community.svg?style=flat-square [gitter-url]: https://gitter.im/angular_rust/community -[tests-badge]: https://img.shields.io/github/workflow/status/angular-rust/ruex/Tests?label=tests&logo=github&style=flat-square +[tests-badge]: https://img.shields.io/github/actions/workflow/status/angular-rust/ruex/tests.yml?label=tests&logo=github&style=flat-square [tests-url]: https://github.com/angular-rust/ruex/actions/workflows/tests.yml [codecov-badge]: https://img.shields.io/codecov/c/github/angular-rust/ruex?logo=codecov&style=flat-square&token=L7KV27OLY0 [codecov-url]: https://codecov.io/gh/angular-rust/ruex diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..63c5e4f --- /dev/null +++ b/build.rs @@ -0,0 +1,8 @@ +fn main() { + match companion::bootstrap() { + Ok(lock) => { + println!("cargo:rerun-if-changed={:?}", lock); + } + Err(_) => println!("already launched"), + } +} diff --git a/macro/Cargo.toml b/macro/Cargo.toml index 751cf67..55e448b 100644 --- a/macro/Cargo.toml +++ b/macro/Cargo.toml @@ -1,14 +1,21 @@ [package] name = "ruex-macro" -version = "0.1.0" +description = "Proc-macro for RUEX" +version = "0.1.3" authors = ["Victor Dudochkin "] edition = "2018" +license = "MPL-2.0" [lib] proc-macro = true [dependencies] proc-macro2 = "1.0" -syn = { version = "1.0", features = ["full"]} +syn = { version = "1.0", features = ["full", "fold", "extra-traits", "visit", "visit-mut"] } quote = "1.0" proc-macro-error = "1.0" +serde_tokenstream = "0.1" +syn-serde = { version = "0.2", features = ["json"] } +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1.0" } +rust-companion = "0" \ No newline at end of file diff --git a/macro/examples/aop.rs b/macro/examples/aop.rs deleted file mode 100644 index c5d46e6..0000000 --- a/macro/examples/aop.rs +++ /dev/null @@ -1,58 +0,0 @@ -use ruex_macro::*; - -// #[Aspect { -// advice: "aspects::TestAspect", -// before: "before(val,val2)", -// after: "after()" -// }] -// fn test_aop(&self, val: i32) -> Result { -// let joint_point = move || { -// println!("Closure {:?} with {}", self, val) -// }; - -// joint_point(); - -// Ok(String::from("Good")) -// } - -#[derive(Debug)] -enum AppErr { - WrongParam, -} - -mod aspects { - use super::AppErr; - - struct TestAspect; - - impl TestAspect { - fn before(val: i32) -> Result<(), AppErr> { - println!("called before"); - Ok(()) - } - - fn after(val: Result) -> Result { - println!("called after"); - Ok(String::from("Here")) - } - } -} - -#[derive(Debug)] -struct AopExample; - -impl AopExample { - #[Aspect { - advice: "aspects::TestAspect", - before: "before(val)", - after: "after()" - }] - fn test_aop(&self, val: i32) -> Result { - Ok(String::from("Good")) - } -} - -fn main() { - let ex = AopExample; - println!("HERE {:?}", ex.test_aop(10)); -} \ No newline at end of file diff --git a/macro/src/impls/compose.rs b/macro/src/impls/compose.rs new file mode 100644 index 0000000..9e6628a --- /dev/null +++ b/macro/src/impls/compose.rs @@ -0,0 +1,186 @@ +use std::net::UdpSocket; + +// use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::{ + parse::{Parse, ParseStream, Result}, + punctuated::Punctuated, + Data, DeriveInput, Field, Fields, Ident, Path, Token, TraitItem, +}; + +use companion::{companion_addr, Response, Task}; + +use crate::MAX_UDP_PAYLOAD; + +#[derive(Default, Debug)] +struct DelegateArgs { + paths: Vec, +} + +impl Parse for DelegateArgs { + fn parse(input: ParseStream) -> Result { + let items; + syn::parenthesized!(items in input); + + let types: Punctuated = items.parse_terminated(syn::Path::parse).unwrap(); + let paths = types.into_iter().collect::>(); + + Ok(DelegateArgs { paths }) + } +} + +struct DelegateVar { + var: Ident, + args: Vec, +} + +fn filter_fields(field: &Field) -> Option { + if let Some(ident) = &field.ident { + let args: Vec = field + .attrs + .iter() + .filter_map(|attr| { + let init = String::new(); + let attribute = attr.path.segments.iter().fold(init, |acc, item| { + if acc.is_empty() { + format!("{}", item.ident) + } else { + format!("{acc}::{}", item.ident) + } + }); + + if attribute == "delegate" { + match syn::parse2::(attr.tokens.clone()) { + Ok(path) => Some(path), + Err(_) => { + panic!("usage: #[delegate(std::fmt::Display, Debug)]"); + } + } + } else { + None + } + }) + .collect(); + Some(DelegateVar { + var: ident.clone(), + args, + }) + } else { + None + } +} + +fn collect(data: &Data) -> Vec { + match &data { + Data::Struct(datastruct) => match &datastruct.fields { + Fields::Named(fields) => { + let collected: Vec = + fields.named.iter().filter_map(filter_fields).collect(); + collected + } + Fields::Unnamed(_) => todo!(), + Fields::Unit => todo!(), + }, + Data::Enum(_) => todo!(), + Data::Union(_) => todo!(), + } +} + +fn generate(var: &Ident, items: &Vec) -> Vec { + items + .iter() + .filter_map(|item| { + if let TraitItem::Method(method) = item { + Some(&method.sig) + } else { + None + } + }) + .map(|signature| { + let syn::Signature { + ident: sig, inputs, .. + } = &signature; + let inputs = inputs + .iter() + .filter_map(|arg| match arg { + syn::FnArg::Receiver(_) => None, + syn::FnArg::Typed(val) => { + if let syn::Pat::Ident(pat) = val.pat.as_ref() { + Some(pat.ident.clone()) + } else { + None + } + } + }) + .collect::>(); + + quote! { + #signature { + self.#var.#sig(#(#inputs), *) + } + } + }) + .collect::>() +} + +pub(crate) fn compose(item: TokenStream2) -> TokenStream2 { + let input: DeriveInput = syn::parse2(item.clone()).unwrap(); + + let DeriveInput { ident, data, .. } = input; + + let collected = collect(&data); + + let addr = companion_addr(); + + let socket = UdpSocket::bind("[::]:0").unwrap(); + socket.connect(addr).unwrap(); + let mut buf = [0; MAX_UDP_PAYLOAD]; + + let impls: Vec = collected + .iter() + .map(|item| { + let var = &item.var; + item.args + .iter() + .map(|item| { + item.paths + .iter() + .map(|path| { + let str_path = path + .segments + .iter() + .map(|item| item.ident.to_string()) + .collect::>() + .join("::"); + + socket.send(&Task::Get(&str_path).as_bytes()).unwrap(); + let (len, _src) = socket.recv_from(&mut buf).unwrap(); + let resp = Response::from(&buf[..len]); + + if let Response::String(data) = resp { + let def: syn::ItemTrait = syn_serde::json::from_str(&data).unwrap(); + + let syn::ItemTrait { items, .. } = def; + let methods = generate(&var, &items); + + quote! { + impl #path for #ident { + #(#methods)* + } + } + } else { + panic!("Trait `{}` is not registered", str_path) + } + }) + .collect::>() + }) + .flatten() + .collect::>() + }) + .flatten() + .collect(); + quote! { + #(#impls)* + } +} diff --git a/macro/src/impls/contract_aspect.rs b/macro/src/impls/contract_aspect.rs new file mode 100644 index 0000000..1430d9c --- /dev/null +++ b/macro/src/impls/contract_aspect.rs @@ -0,0 +1,967 @@ +#![allow(dead_code)] +use std::{ + collections::{HashMap, VecDeque}, + fmt, + net::UdpSocket, +}; + +// use proc_macro::TokenStream; +use proc_macro2::{Punct, Spacing, TokenStream as TokenStream2, TokenTree}; +use quote::{format_ident, quote}; +use syn::{ + parse::{Parse, ParseStream, Result}, + punctuated::Punctuated, + visit_mut::VisitMut, + Attribute, Expr, ExprLit, ExprPath, Lit, Meta, MetaNameValue, PathSegment, Token, TraitItem, + TraitItemMethod, +}; + +use companion::{companion_addr, Response, Task}; + +use crate::MAX_UDP_PAYLOAD; + +pub struct AspectJointPoint<'a> { + pub stream: &'a TokenStream2, +} + +impl<'a> VisitMut for AspectJointPoint<'a> { + fn visit_expr_mut(&mut self, item: &mut Expr) { + if let Expr::Call(ref node) = item { + if let Expr::Path(call) = node.func.as_ref() { + let segments = &call.path.segments; + if segments.len() == 2 { + if let (Some(first), Some(last)) = (segments.first(), segments.last()) { + if first.ident.to_string() == "AspectJointPoint" + && last.ident.to_string() == "proceed" + { + // prepare variables + let stream = self.stream; + *item = syn::parse_quote!(#stream); + } + } + } + } + } + + // Delegate to the default impl to visit nested expressions. + syn::visit_mut::visit_expr_mut(self, item); + } +} + +#[derive(Default, Debug)] +pub struct Aspect { + pub name: String, + pub docs: VecDeque, + pub before: Option, + pub after: Option, + pub around: Option, +} + +impl Aspect { + pub fn new(attr: TokenStream2) -> Self { + // prefetch from attrs + match syn::parse2::(attr.clone()) { + Ok(_) => {} + Err(_) => panic!("Usage: #[aspect(std::fmt::Display)]"), + } + + let key = attr.to_string().replace(" :: ", "::"); + + let addr = companion_addr(); + + let socket = UdpSocket::bind("[::]:0").unwrap(); + socket.connect(addr).unwrap(); + let mut buf = [0; MAX_UDP_PAYLOAD]; + + socket.send(&Task::Get(&key).as_bytes()).unwrap(); + let (len, _src) = socket.recv_from(&mut buf).unwrap(); + let resp = Response::from(&buf[..len]); + + if let Response::String(data) = resp { + let item_trait: syn::ItemTrait = syn_serde::json::from_str(&data).unwrap(); + let mut aspect = Aspect::default(); + aspect.name = key; + aspect.docs = collect_docs(&item_trait.attrs); + + // construct proto + for item in item_trait.items.iter() { + match item { + TraitItem::Method(method) => { + // here + let name = method.sig.ident.to_string(); + // println!("Sig {}", name); + match name.as_str() { + "before" => aspect.before = Some(method.clone()), + "after" => aspect.after = Some(method.clone()), + "around" => aspect.around = Some(method.clone()), + _ => { + println!("Incompatible Aspect trait") + } + } + } + _ => { + println!("Incompatible Aspect trait") + } + } + } + + aspect + } else { + panic!("Aspect is not registered") + } + } + + pub fn documentation(&self) -> VecDeque { + let mut output: VecDeque = VecDeque::new(); + + let mut before_docs: VecDeque = VecDeque::new(); + let mut after_docs: VecDeque = VecDeque::new(); + let mut around_docs: VecDeque = VecDeque::new(); + + if let Some(ref method) = self.before { + before_docs = collect_docs(&method.attrs); + } + + if let Some(ref method) = self.around { + around_docs = collect_docs(&method.attrs); + } + + if let Some(ref method) = self.after { + after_docs = collect_docs(&method.attrs); + } + + // if there something in documentations + if !self.docs.is_empty() + || !before_docs.is_empty() + || !after_docs.is_empty() + || !around_docs.is_empty() + { + output.push_back(format!(" ### {}", self.name)); + self.docs.iter().for_each(|item| { + output.push_back(item.clone()); + }); + + if !before_docs.is_empty() { + // output.push_back(String::from("")); + output.push_back(String::from(" - __Before:__")); + + before_docs.iter().for_each(|item| { + output.push_back(item.clone()); + }); + } + + if !around_docs.is_empty() { + // output.push_back(String::from("")); + output.push_back(String::from(" - __Around:__")); + + around_docs.iter().for_each(|item| { + output.push_back(item.clone()); + }); + } + + if !after_docs.is_empty() { + // output.push_back(String::from("")); + output.push_back(String::from(" - __After:__")); + + after_docs.iter().for_each(|item| { + output.push_back(item.clone()); + }); + } + } + + output + } +} + +fn collect_docs(attrs: &Vec) -> VecDeque { + let mut docs: VecDeque = VecDeque::new(); + attrs.iter().for_each(|attr| { + // Collect docs attributes + if attr.path.is_ident("doc") { + match attr.parse_meta().unwrap() { + Meta::NameValue(MetaNameValue { + lit: Lit::Str(lit_str), + .. + }) => { + docs.push_back(lit_str.value()); + } + _ => {} + } + } + }); + docs +} + +// detect ranges separated by implication and return +fn stream_ranges(input: &Vec) -> Vec<(usize, usize)> { + let mut ranges: Vec<(usize, usize)> = vec![]; + + let mut tail = &TokenTree::Punct(Punct::new(' ', Spacing::Alone)); + let mut start_current = 0; + + // split for -> + for (idx, attr) in input.iter().enumerate() { + if let TokenTree::Punct(cur) = attr { + if cur.as_char() == '>' && cur.spacing() == Spacing::Alone { + if let TokenTree::Punct(prev) = tail { + if prev.as_char() == '-' && prev.spacing() == Spacing::Joint { + ranges.push((start_current, idx - 1)); + start_current = idx + 1; + } + } + } + } + + tail = attr; + } + + if start_current < input.len() { + ranges.push((start_current, input.len())); + } + + ranges +} + +/// Checking-mode of a contract. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum Mode { + /// Always check contract + Always, + /// Never check contract + Disabled, + /// Check contract only in debug builds + Debug, + /// Check contract only in `#[cfg(test)]` configurations + Test, + /// Check the contract and print information upon violation, but don't abort + /// the program. + LogOnly, +} + +impl Mode { + /// Return the prefix of attributes of `self` mode. + pub fn name(self) -> Option<&'static str> { + match self { + Mode::Always => Some(""), + Mode::Disabled => None, + Mode::Debug => Some("debug_"), + Mode::Test => Some("test_"), + Mode::LogOnly => None, + } + } + + /// Computes the contract type based on feature flags. + pub fn final_mode(self) -> Self { + // disabled ones can't be "forced", test ones should stay test, no + // matter what. + if self == Mode::Disabled || self == Mode::Test { + return self; + } + + if cfg!(feature = "disable_contracts") { + Mode::Disabled + } else if cfg!(feature = "override_debug") { + // log is "weaker" than debug, so keep log + if self == Mode::LogOnly { + self + } else { + Mode::Debug + } + } else if cfg!(feature = "override_log") { + Mode::LogOnly + } else { + self + } + } +} + +/// The different contract types. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Type { + Requires, + Ensures, + Invariant, + Aspect, +} + +impl Type { + /// Get the name that is used as a message-prefix on violation of a + /// contract. + pub fn message_name(self) -> &'static str { + match self { + Type::Requires => "Pre-condition", + Type::Ensures => "Post-condition", + Type::Invariant => "Invariant", + Type::Aspect => "Invariant", + } + } + + /// Determine the type and mode of an identifier. + pub fn type_and_mode(ident: &str) -> Option<(Type, Mode)> { + match ident { + "aspect" => Some((Type::Aspect, Mode::Always)), + "requires" => Some((Type::Requires, Mode::Always)), + "ensures" => Some((Type::Ensures, Mode::Always)), + "invariant" => Some((Type::Invariant, Mode::Always)), + "debug_requires" => Some((Type::Requires, Mode::Debug)), + "debug_ensures" => Some((Type::Ensures, Mode::Debug)), + "debug_invariant" => Some((Type::Invariant, Mode::Debug)), + "test_requires" => Some((Type::Requires, Mode::Test)), + "test_ensures" => Some((Type::Ensures, Mode::Test)), + "test_invariant" => Some((Type::Invariant, Mode::Test)), + _ => None, + } + } +} + +// Contain single part of contract case as vector of Rule expressions +#[derive(Debug)] +pub struct CaseRule(Vec); + +impl CaseRule { + fn add(&mut self, value: RuleExpression) { + self.0.push(value); + } +} + +impl Default for CaseRule { + fn default() -> Self { + Self(Default::default()) + } +} + +// Describe rule expression +#[derive(Debug)] +enum RuleExpression { + If(Expr), + Expr(Expr), + // Desc(String), +} + +pub struct Contract { + pub ty: Type, + pub mode: Mode, + pub desc: Option, + pub rules: Vec, + // variable declarations + pub decls: HashMap, +} + +impl Contract { + pub fn syntax(&self) -> Vec { + let assert = match self.mode { + Mode::Always => format_ident!("assert"), + Mode::Debug | Mode::Test => { + format_ident!("debug_assert") + } + Mode::Disabled | Mode::LogOnly => return vec![], + }; + + self.rules + .iter() + .map(|rule| { + let mut holder = quote! {}; + + for expr in rule.0.iter().rev() { + match expr { + RuleExpression::If(e) => { + holder = quote! { + if (#e) { + #holder + } + }; + } + RuleExpression::Expr(e) => match self.desc { + Some(ref desc) => { + holder = quote! { + #assert!(#e, #desc); + }; + } + None => { + holder = quote! { + #assert!(#e); + }; + } + }, + } + } + holder + }) + .collect() + } +} + +impl fmt::Debug for Contract { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.desc { + Some(desc) => { + write!( + f, + "{:?}:{:?}, {} rules [{desc}]", + self.mode, + self.ty, + self.rules.len() + ) + } + None => { + write!( + f, + "{:?}:{:?}, {} rules", + self.mode, + self.ty, + self.rules.len() + ) + } + } + } +} + +impl Contract { + pub fn new(ty: Type, mode: Mode) -> Self { + Self { + ty, + mode, + desc: None, + rules: Vec::new(), + decls: HashMap::new(), + } + } + + // Parse attribures from TokenStream into rules + // + pub fn parse_attributes(&mut self, stream: TokenStream2) { + // should split stream by implication first, instead coma + let stream = stream.into_iter().collect::>(); + + let ranges = stream_ranges(&stream); + + // `peekable` to look at the next element of the iterator without consuming it + let mut range_iter = ranges.iter().peekable(); + + // expressions placeholder + let mut rule = CaseRule::default(); + while let Some(range) = range_iter.next() { + let stream = stream[range.0..range.1] + .iter() + .map(|x| x.clone()) + .collect::(); + let Segment { expressions } = syn::parse2(stream).unwrap(); + + let is_last_segment = range_iter.peek().is_none(); + + let mut expr_iter = expressions.iter().peekable(); + while let Some(expr) = expr_iter.next() { + let is_last_expr = expr_iter.peek().is_none(); + + if !is_last_segment && is_last_expr { + match expr { + Expr::Lit(_) => panic!("Description should be last element"), + _ => { + let mut expr = expr.clone(); + let mut replacer = OldPseudo::default(); + replacer.visit_expr_mut(&mut expr); + + if self.ty != Type::Ensures && !replacer.items.is_empty() { + panic!("Only ensures support 'old' pseudo-expressions") + } + + replacer.items.into_iter().for_each(|(k, v)| { + self.decls.insert(k, v); + }); + + rule.add(RuleExpression::If(expr)); + } + } + } else if is_last_segment && !is_last_expr { + match expr { + Expr::Lit(_) => panic!("Description should be last element"), + _ => { + let mut expr = expr.clone(); + let mut replacer = OldPseudo::default(); + replacer.visit_expr_mut(&mut expr); + + if self.ty != Type::Ensures && !replacer.items.is_empty() { + panic!("Only ensures support 'old' pseudo-expressions") + } + + replacer.items.into_iter().for_each(|(k, v)| { + self.decls.insert(k, v); + }); + + rule.add(RuleExpression::Expr(expr)); + self.rules.push(rule); + rule = CaseRule::default(); + } + } + } else if is_last_segment && is_last_expr { + // get desc if exists + match expr { + Expr::Lit(ExprLit { + lit: Lit::Str(token), + .. + }) => { + self.desc = Some(token.value()); + } + _ => { + let mut expr = expr.clone(); + let mut replacer = OldPseudo::default(); + replacer.visit_expr_mut(&mut expr); + + if self.ty != Type::Ensures && !replacer.items.is_empty() { + panic!("Only ensures support 'old' pseudo-expressions") + } + + replacer.items.into_iter().for_each(|(k, v)| { + self.decls.insert(k, v); + }); + + rule.add(RuleExpression::Expr(expr)); + self.rules.push(rule); + rule = CaseRule::default(); + } + } + } else { + match expr { + Expr::Lit(_) => panic!("Description should be last element"), + _ => { + let mut expr = expr.clone(); + let mut replacer = OldPseudo::default(); + replacer.visit_expr_mut(&mut expr); + + if self.ty != Type::Ensures && !replacer.items.is_empty() { + panic!("Only ensures support 'old' pseudo-expressions") + } + + replacer.items.into_iter().for_each(|(k, v)| { + self.decls.insert(k, v); + }); + + rule.add(RuleExpression::Expr(expr)); + self.rules.push(rule); + rule = CaseRule::default(); + } + } + } + } + } + } +} + +// faster trim whitespace +pub fn trim_whitespace(s: &str) -> String { + let mut new_str = s.trim().to_owned(); + let mut prev = ' '; // The initial value doesn't really matter + new_str.retain(|ch| { + let result = ch != ' ' || prev != ' '; + prev = ch; + result + }); + new_str +} + +#[derive(Debug, Default)] +struct OldPseudo { + items: HashMap, +} + +impl VisitMut for OldPseudo { + fn visit_expr_mut(&mut self, item: &mut Expr) { + if let Expr::Call(ref node) = item { + if let syn::Expr::Path(call) = node.func.as_ref() { + let segments = &call.path.segments; + if let Some(path) = segments.first() { + if path.ident.to_string() == "old" { + // println!("Call: {item:?}"); + let name = format!("{}", quote!(#node)); + let cleaned: String = name + .chars() + .filter_map(|x| match x { + 'A'..='Z' | 'a'..='z' | '0'..='9' | ' ' => Some(x), + _ => None, + }) + .collect(); + + let name = trim_whitespace(&cleaned) + .split(' ') + .filter(|s| !s.is_empty()) + .collect::>() + .join("_"); + + let var = format_ident!("{name}"); + + let old_arg = node + .args + .first() + .expect("The 'old' pseudo-function have exactly one parameter"); + + // store node as `old` pseudo-expressions + self.items.insert(name, old_arg.clone()); + + let expr: ExprPath = syn::parse_quote!(#var); + *item = Expr::Path(expr); + } + } + } + } + // Delegate to the default impl to visit nested expressions. + syn::visit_mut::visit_expr_mut(self, item); + } +} + +#[derive(Debug, Clone)] +pub struct Segment { + pub expressions: Vec, +} + +impl Parse for Segment { + fn parse(input: ParseStream) -> Result { + let vars = Punctuated::::parse_terminated(input).unwrap(); + let expressions: Vec = vars.into_iter().collect(); + + // println!("{items:#?}"); + Ok(Segment { expressions }) + } +} + +#[derive(Default, Debug)] +pub struct ContractAspectState { + pub docs: VecDeque, + pub requires: Vec, + pub invariants: Vec, + pub ensures: Vec, + pub aspects: Vec, +} + +impl ContractAspectState { + // Process rest of contract attributes + // and keep others, to generate new attributes set + pub fn process(&mut self, attrs: &Vec) -> Vec { + attrs + .iter() + .filter_map(|attr| { + // let len = attr.path.segments.len(); + if let Some(PathSegment { ident, .. }) = attr.path.segments.last() { + let pair = Type::type_and_mode(&ident.to_string()); + if let Some((ty, mode)) = pair { + // create case + match ty { + Type::Requires | Type::Ensures | Type::Invariant => { + let mut case = Contract::new(ty, mode); + let tokens = attr.tokens.clone().into_iter().collect::>(); + assert!( + tokens.len() == 1, + "Wrong contract sintax: not parenthesized expression" + ); + + if let Some(TokenTree::Group(group)) = tokens.first() { + case.parse_attributes(group.stream()); + match ty { + Type::Requires => self.requires.push(case), + Type::Ensures => self.ensures.push(case), + Type::Invariant => self.invariants.push(case), + _ => unreachable!(), + } + } else { + panic!("Wrong contract sintax: not parenthesized expression") + } + } + Type::Aspect => { + let tokens = attr.tokens.clone().into_iter().collect::>(); + assert!( + tokens.len() == 1, + "Wrong aspect sintax: not parenthesized expression" + ); + if let Some(TokenTree::Group(group)) = tokens.first() { + let aspect = Aspect::new(group.stream()); + self.aspects.push(aspect); + } else { + panic!("Wrong contract sintax: not parenthesized expression") + } + } + }; + + // Eat contract attribute + None + } else { + // Attribute is not contract + // Collect docs attributes + if attr.path.is_ident("doc") { + match attr.parse_meta().unwrap() { + Meta::NameValue(MetaNameValue { + lit: Lit::Str(lit_str), + .. + }) => { + // println!("{}", lit_str.value()); + self.docs.push_back(lit_str.value()); + None + } + _ => Some(attr.clone()), + } + } else { + Some(attr.clone()) + } + } + } else { + // Attribute without path + unreachable!() + } + }) + .collect::>() + } + + // collect variable declarations from ensure cases + pub fn variables(&self) -> Vec { + let mut decls: HashMap = HashMap::new(); + self.ensures.iter().for_each(|case| { + case.decls.iter().for_each(|(name, expr)| { + decls.insert(name.to_string(), expr.clone()); + }); + }); + + decls + .into_iter() + .map(|(name, expr)| { + let name = format_ident!("{name}"); + quote! { + let #name = #expr; + } + }) + .collect::>() + } +} + +pub trait SyntaxAndDocs { + fn generate(&self) -> (Vec, Vec); +} + +impl SyntaxAndDocs for Vec { + fn generate(&self) -> (Vec, Vec) { + let mut docs = vec![]; + let stream = self + .iter() + .map(|case| { + if let Some(ref desc) = case.desc { + docs.push(format!(" - {}", desc)) + } + case.syntax() + }) + .flatten() + .collect::>(); + (stream, docs) + } +} + +// process macro attribute by initiating currentt case and handle rest of cases +pub(crate) fn contracts_aspect( + ty: Type, + mode: Mode, + attrs: TokenStream2, + item: TokenStream2, +) -> TokenStream2 { + let mut state: ContractAspectState = ContractAspectState::default(); + + // process primary contract case + + match ty { + Type::Requires | Type::Ensures | Type::Invariant => { + let mut case = Contract::new(ty, mode); + case.parse_attributes(attrs); + + match ty { + Type::Requires => state.requires.push(case), + Type::Ensures => state.ensures.push(case), + Type::Invariant => state.invariants.push(case), + _ => unreachable!(), + } + } + Type::Aspect => { + state.aspects.push(Aspect::new(attrs)); + } + } + + // process rest of contract cases + let input: syn::ItemFn = syn::parse2(item.clone().into()).unwrap(); + + let syn::ItemFn { + attrs, + vis, + sig, + block, + } = input; + + let attrs = state.process(&attrs); + + let variables = state.variables(); + + // генерация синтаксиса и документации + let mut contract_docs: VecDeque = VecDeque::new(); + + let (requires, docs) = state.requires.generate(); + contract_docs.extend(docs); + + let (invariants, docs) = state.invariants.generate(); + contract_docs.extend(docs); + + let (ensures, docs) = state.ensures.generate(); + contract_docs.extend(docs); + + if !contract_docs.is_empty() { + contract_docs.push_front(String::from(" # Contract")); + } + + let before = state + .aspects + .iter() + .filter_map(|aspect| { + if let Some(item) = &aspect.before { + item.default.as_ref().map(|block| { + let stmts = &block.stmts; + quote! { + #(#stmts)* + } + }) + } else { + None + } + }) + .collect::>(); + + let after = state + .aspects + .iter() + .rev() + .filter_map(|aspect| { + if let Some(item) = &aspect.after { + item.default.as_ref().map(|block| { + let stmts = &block.stmts; + quote! { + #(#stmts)* + } + }) + } else { + None + } + }) + .collect::>(); + + // Here we should deal with call name ))) + // For traits we need only change some in output + let mut around = quote! {inner()}; + let mut has_around = false; + + let mut it = state.aspects.iter().rev().peekable(); + while let Some(aspect) = it.next() { + if let Some(item) = &aspect.around { + item.default.as_ref().map(|block| { + let mut replacer = AspectJointPoint { stream: &around }; + + let mut stmts = block.stmts.clone(); + + for stmt in stmts.iter_mut() { + replacer.visit_stmt_mut(stmt); + } + + if it.peek().is_none() { + around = quote!(#(#stmts)*); + } else { + around = quote!({#(#stmts)*}); + } + has_around = true; + }); + } + } + + let attrs = { + let mut new_attrs: Vec = Vec::new(); + + if !state.docs.is_empty() { + let mut it = state.docs.iter().peekable(); + while let Some(comment) = it.next() { + if it.peek().is_some() { + new_attrs.push(syn::parse_quote!(#[doc = #comment])); + } else if !comment.trim().is_empty() { + new_attrs.push(syn::parse_quote!(#[doc = #comment])); + } + } + new_attrs.push(syn::parse_quote!(#[doc = ""])); + } + + if !contract_docs.is_empty() { + let mut it = contract_docs.iter().peekable(); + while let Some(comment) = it.next() { + if it.peek().is_some() { + new_attrs.push(syn::parse_quote!(#[doc = #comment])); + } else if !comment.trim().is_empty() { + new_attrs.push(syn::parse_quote!(#[doc = #comment])); + } + } + new_attrs.push(syn::parse_quote!(#[doc = ""])); + } + + let mut aspect_docs: VecDeque = VecDeque::new(); + // TODO: Aspects + state.aspects.iter().for_each(|aspect| { + // + let mut docs = aspect.documentation(); + if !docs.is_empty() { + docs.push_back(String::new()); + aspect_docs.extend(docs); + } + }); + + if !aspect_docs.is_empty() { + aspect_docs.push_front(String::from(" # Aspects")); + let mut it = aspect_docs.iter().peekable(); + while let Some(comment) = it.next() { + if it.peek().is_some() { + new_attrs.push(syn::parse_quote!(#[doc = #comment])); + } else if !comment.trim().is_empty() { + new_attrs.push(syn::parse_quote!(#[doc = #comment])); + } + } + new_attrs.push(syn::parse_quote!(#[doc = ""])); + } + attrs.iter().cloned().for_each(|attr| { + new_attrs.push(attr); + }); + new_attrs + }; + + let stmts = &block.stmts; + let result = if has_around { + quote! { + let result = { + #around + }; + } + } else { + quote! { + let result = inner(); + } + }; + + quote! { + #(#attrs)* #vis #sig { + + #(#requires)* + #(#invariants)* + + #(#variables)* + + let inner = || { + #(#stmts)* + }; + + #(#before)* + + #result + + #(#after)* + + #(#invariants)* + #(#ensures)* + + result + } + } +} diff --git a/macro/src/impls/decorate.rs b/macro/src/impls/decorate.rs new file mode 100644 index 0000000..f41359d --- /dev/null +++ b/macro/src/impls/decorate.rs @@ -0,0 +1,109 @@ +use std::{error, fmt, result}; + +use proc_macro::TokenStream; +use proc_macro2::TokenTree; +use syn::*; + +type Result = result::Result>; + +#[derive(Debug, PartialEq)] +enum DecoratorError { + InvaludTokenStream, + DecoratorNotFound, +} + +impl std::fmt::Display for DecoratorError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result { + match self { + DecoratorError::InvaludTokenStream => write!(f, "Invalid token stream"), + DecoratorError::DecoratorNotFound => write!(f, "Decorator name not found"), + } + } +} + +impl error::Error for DecoratorError {} + +#[derive(Debug, PartialEq)] +enum DecoratorAttr { + Fixed { name: Ident }, + Parametric { name: Ident, args: Vec }, +} + +impl DecoratorAttr { + fn parse(attr: proc_macro2::TokenStream) -> Result { + let mut ident = None; + let mut args = Vec::new(); + for at in attr { + match at { + TokenTree::Ident(id) => { + ident = Some(id); + } + TokenTree::Group(grp) => { + if ident.is_none() { + return Err(DecoratorError::InvaludTokenStream)?; + } + for t in grp.stream() { + if let Ok(expr) = syn::parse2(t.into()) { + args.push(expr); + } + } + } + _ => return Err(DecoratorError::InvaludTokenStream)?, + } + } + if let Some(name) = ident { + if args.is_empty() { + Ok(DecoratorAttr::Fixed { name }) + } else { + Ok(DecoratorAttr::Parametric { name, args }) + } + } else { + return Err(DecoratorError::DecoratorNotFound)?; + } + } +} + +pub(crate) fn decorate(attr: TokenStream, func: TokenStream) -> TokenStream { + let func = func.into(); + let item_fn: ItemFn = syn::parse(func).expect("Input is not a function"); + let vis = &item_fn.vis; + let ident = &item_fn.sig.ident; + let block = &item_fn.block; + + let inputs = item_fn.sig.inputs; + let output = item_fn.sig.output; + + let input_values: Vec<_> = inputs + .iter() + .map(|arg| match arg { + &FnArg::Typed(ref val) => &val.pat, + _ => unimplemented!("#[decorate] cannot be used with associated function"), + }) + .collect(); + + let attr = DecoratorAttr::parse(attr.into()).expect("Failed to parse attribute"); + let caller = match attr { + DecoratorAttr::Fixed { name } => { + quote::quote! { + #vis fn #ident(#inputs) #output { + let f = #name(deco_internal); + return f(#(#input_values,) *); + + fn deco_internal(#inputs) #output #block + } + } + } + DecoratorAttr::Parametric { name, args } => { + quote::quote! { + #vis fn #ident(#inputs) #output { + let deco = #name(#(#args,) *); + let f = deco(deco_internal); + return f(#(#input_values,) *); + + fn deco_internal(#inputs) #output #block + } + } + } + }; + caller.into() +} \ No newline at end of file diff --git a/macro/src/impls/delegate.rs b/macro/src/impls/delegate.rs new file mode 100644 index 0000000..fa13a4c --- /dev/null +++ b/macro/src/impls/delegate.rs @@ -0,0 +1,213 @@ +use proc_macro2::{Group, Span, TokenStream, TokenTree}; +use quote::{quote, quote_spanned, ToTokens}; +use syn::spanned::Spanned; +use syn::{ + parse_macro_input, Expr, ExprParen, FnArg, ImplItem, ImplItemMethod, ItemImpl, Pat, ReturnType, +}; + +fn delegate_input(input: TokenStream, receiver: &Expr) -> TokenStream { + if let Ok(input) = syn::parse2::(input.clone()) { + return delegate_impl_block(input, receiver); + } + if let Ok(input) = syn::parse2::(input.clone()) { + return delegate_method(input, receiver); + } + let mut tokens = input.into_iter(); + let first_non_attr_token = 'outer: loop { + match tokens.next() { + None => break None, + Some(TokenTree::Punct(p)) if p.as_char() == '#' => {} + Some(token) => break Some(token), + } + loop { + match tokens.next() { + None => break 'outer None, + Some(TokenTree::Punct(_)) => {} + Some(TokenTree::Group(_)) => continue 'outer, + Some(token) => break 'outer Some(token), + } + } + }; + if let Some(token) = first_non_attr_token { + let msg = match &token { + TokenTree::Ident(ident) if ident == "impl" => "invalid impl block for #[delegate]", + TokenTree::Ident(ident) if ident == "fn" => "invalid method for #[delegate]", + _ => "expected an impl block or method inside impl block", + }; + quote_spanned! { token.span() => compile_error!(#msg); } + } else { + panic!("unexpected eof") + } +} + +fn delegate_impl_block(input: ItemImpl, receiver: &Expr) -> TokenStream { + let ItemImpl { + attrs, + defaultness, + unsafety, + impl_token, + mut generics, + trait_, + self_ty, + brace_token: _, + items, + } = input; + let where_clause = generics.where_clause.take(); + let trait_ = trait_.map(|(bang, path, for_)| quote!(#bang #path #for_)); + let items = items.into_iter().map(|item| { + let method = match item { + ImplItem::Method(m) => m, + _ => return item.into_token_stream(), + }; + delegate_method(method, receiver) + }); + + quote! { + #(#attrs)* #defaultness #unsafety #impl_token #generics #trait_ #self_ty #where_clause { + #(#items)* + } + } +} + +fn delegate_method(input: ImplItemMethod, receiver: &Expr) -> TokenStream { + let ImplItemMethod { + mut attrs, + vis, + defaultness, + sig, + block: _, + } = input; + let mut errors = TokenStream::new(); + let mut push_error = |span: Span, msg: &'static str| { + errors.extend(quote_spanned! { span => compile_error!(#msg); }); + }; + // Parse attributes. + let mut has_inline = false; + let mut has_into = false; + let mut call_name = None; + attrs.retain(|attr| { + let path = &attr.path; + if path.is_ident("inline") { + has_inline = true; + } else if path.is_ident("into") { + if !attr.tokens.is_empty() { + push_error(attr.tokens.span(), "unexpected argument"); + } + if has_into { + push_error(attr.span(), "duplicate #[into] attribute"); + } + has_into = true; + return false; + } else if path.is_ident("call") { + match syn::parse2::(attr.tokens.clone()) { + Ok(expr) if expr.attrs.is_empty() => { + let inner = expr.expr; + match &*inner { + Expr::Path(path) if path.attrs.is_empty() && path.qself.is_none() => { + if let Some(ident) = path.path.get_ident() { + if call_name.is_some() { + push_error(attr.span(), "duplicate #[call] attribute"); + } + call_name = Some(ident.clone()); + } else { + push_error( + inner.span(), + "invalid argument, expected an identifier", + ); + } + } + _ => push_error(inner.span(), "invalid argument, expected an identifier"), + } + } + _ => push_error(attr.tokens.span(), "invalid argument"), + } + return false; + } + true + }); + // Mark method always inline if it's not otherwise specified. + let inline = if !has_inline { + quote!(#[inline(always)]) + } else { + quote!() + }; + let mut inputs = sig.inputs.iter(); + // Extract the self token. + let self_token = match inputs.next() { + Some(FnArg::Receiver(receiver)) => receiver.self_token.to_token_stream(), + Some(FnArg::Typed(pat)) => match &*pat.pat { + Pat::Ident(ident) if ident.ident == "self" => ident.ident.to_token_stream(), + _ => { + push_error(pat.span(), "expected self"); + TokenStream::new() + } + }, + None => { + push_error(sig.paren_token.span, "expected self"); + TokenStream::new() + } + }; + // List all parameters. + let args = inputs + .filter_map(|arg| match arg { + FnArg::Typed(pat) => match &*pat.pat { + Pat::Ident(ident) => Some(ident.to_token_stream()), + _ => { + push_error(pat.pat.span(), "expect an identifier"); + None + } + }, + _ => { + push_error(arg.span(), "unexpected argument"); + None + } + }) + .collect::>(); + // Return errors if any. + if !errors.is_empty() { + return errors; + } else { + // Drop it to ensure that we are not pushing anymore into it. + drop(errors); + } + // Generate method call. + let name = call_name.as_ref().unwrap_or(&sig.ident); + // Replace the self token in the receiver with the token we extract above to ensure it comes + // from the right hygiene context. + let receiver = replace_self(receiver.to_token_stream(), &self_token); + let body = quote! { #receiver.#name(#(#args),*) }; + let body = match &sig.output { + ReturnType::Default => quote! { #body; }, + ReturnType::Type(_, ty) if has_into => { + quote! { ::std::convert::Into::<#ty>::into(#body) } + } + _ => body, + }; + quote! { + #(#attrs)* #inline #vis #defaultness #sig { + #body + } + } +} + +fn replace_self(expr: TokenStream, self_token: &TokenStream) -> TokenStream { + expr.into_iter() + .map(|token| match token { + TokenTree::Ident(ident) if ident == "self" => self_token.clone(), + TokenTree::Group(group) => { + let delimiter = group.delimiter(); + let stream = replace_self(group.stream(), self_token); + Group::new(delimiter, stream).into_token_stream() + } + _ => token.into_token_stream(), + }) + .collect() +} + +pub(crate) fn delegate( + attr: proc_macro::TokenStream, + item: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + let receiver = parse_macro_input!(attr as Expr); + delegate_input(item.into(), &receiver).into() +} \ No newline at end of file diff --git a/macro/src/impls/enums.rs b/macro/src/impls/enums.rs new file mode 100644 index 0000000..023ecc7 --- /dev/null +++ b/macro/src/impls/enums.rs @@ -0,0 +1,75 @@ +#![allow(dead_code)] +#![allow(unused_imports)] +#![allow(unused_variables)] +// necessary for the TokenStream::from_str() implementation +use std::{collections::HashMap, net::UdpSocket, path::PathBuf, str::FromStr}; + +use companion::{companion_addr, Response, Task}; +// use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use serde::{Deserialize, Serialize, Serializer}; +use serde_tokenstream::from_tokenstream; +use syn::{Item, ItemFn, ItemStruct, Path}; + +use crate::MAX_UDP_PAYLOAD; + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub(crate) struct ItemLocation { + pub path: String, + pub range: (usize, usize), +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub(crate) struct Enum { + pub supertraits: Vec, + pub items: HashMap, +} + +pub(crate) fn mount( + attrs: TokenStream2, + input: TokenStream2, + location: ItemLocation, +) -> TokenStream2 { + let path: Path = syn::parse2(attrs).unwrap(); + let path = path + .segments + .iter() + .map(|item| item.ident.to_string()) + .collect::>() + .join("::"); + let item: Item = syn::parse2(input.clone()).unwrap(); + match item { + Item::Struct(item) => { + let ident = item.ident.to_string(); + // println!("Place {ident} into {path}"); + let mut buf = [0; MAX_UDP_PAYLOAD]; + + let addr = companion_addr(); + + let socket = UdpSocket::bind("[::]:0").unwrap(); + socket.connect(addr).unwrap(); + + socket.send(&Task::Get(&path).as_bytes()).unwrap(); + let (len, _src) = socket.recv_from(&mut buf).unwrap(); + let resp = Response::from(&buf[..len]); + + if let Response::String(data) = resp { + // println!("{data}"); + let mut data: Enum = serde_json::from_str(&data).unwrap(); + data.items.insert(ident, location); + // println!("{data:#?}"); + + let data = serde_json::to_string(&data).unwrap(); + // println!("{data}"); + socket.send(&Task::Set(&path, &data).as_bytes()).unwrap(); + let (len, _src) = socket.recv_from(&mut buf).unwrap(); + let _resp = Response::from(&buf[..len]); + } else { + panic!("Enum Trait `{}` is not registered", path) + } + } + _ => panic!("Struct only supported for mounting"), + } + input +} diff --git a/macro/src/impls/mock.rs b/macro/src/impls/mock.rs new file mode 100644 index 0000000..11f5eec --- /dev/null +++ b/macro/src/impls/mock.rs @@ -0,0 +1,217 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; + +use quote::{format_ident, quote}; +use syn::{FnArg, Item, ItemTrait, ReturnType, TraitItem, TypeParamBound}; + +fn process_item_trait(mut input: ItemTrait) -> TokenStream2 { + let trait_name = input.ident.clone(); + let mock_name = format_ident!("Mock{}", trait_name); + let builder_name = format_ident!("Mock{}Builder", trait_name); + + // Fix default supertrait + { + let mut has_default = false; + input.supertraits.iter().for_each(|item| { + // + if let TypeParamBound::Trait(val) = item { + let name = val.path.segments.last().unwrap().ident.to_string(); + if &name == "Default" { + has_default = true; + } + } + }); + if !has_default { + let value: syn::TraitBound = syn::parse_quote! {Default}; + input.supertraits.insert(0, TypeParamBound::Trait(value)); + } + } + + // Collect method names and generate mock method + let fields = input + .items + .iter() + .filter_map(|item| { + if let TraitItem::Method(method) = item { + Some(format_ident!("__mock_{}__", method.sig.ident)) + } else { + None + } + }) + .collect::>(); + let mock_method: syn::TraitItemMethod = syn::parse_quote! { + fn mock() -> #builder_name { + #builder_name { + inner: #mock_name { + __mock_fallback__: Default::default(), + #(#fields: None,)* + } + } + } + }; + + let mut builder_methods: Vec = vec![]; + // Generate mock struct + let mock = { + let fields = input + .items + .iter() + .filter_map(|item| { + if let TraitItem::Method(method) = item { + let inputs = method + .sig + .inputs + .iter() + .filter_map(|input| { + if let FnArg::Typed(typed) = input { + Some(typed.ty.clone()) + } else { + None + } + }) + .collect::>(); + let field = format_ident!("__mock_{}__", method.sig.ident); + match method.sig.output { + ReturnType::Default => { + let builder_method = { + let method_name = &method.sig.ident; + quote! { + fn #method_name(mut self, when: F) -> Self + where + F: Fn(#(#inputs),*) + 'static, + { + self.inner.#field = Some(Box::new(when)); + self + } + } + }; + builder_methods.push(builder_method); + Some(quote! {#field: Option>}) + } + ReturnType::Type(_, ref ty) => { + let output = ty.clone(); + let builder_method = { + let method_name = &method.sig.ident; + quote! { + fn #method_name(mut self, when: F) -> Self + where + F: Fn(#(#inputs),*) -> #output + 'static, + { + self.inner.#field = Some(Box::new(when)); + self + } + } + }; + builder_methods.push(builder_method); + Some(quote! {#field: Option #output>>}) + } + } + } else { + None + } + }) + .collect::>(); + + quote! { + #[derive(Default)] + struct #mock_name + where + T: #trait_name, + { + __mock_fallback__: T, + #(#fields),* + } + } + }; + + let mock_impl = { + let methods = input + .items + .iter() + .filter_map(|item| { + if let TraitItem::Method(method) = item { + let sig = method.sig.clone(); + let args = sig + .inputs + .iter() + .filter_map(|input| { + if let FnArg::Typed(syn::PatType { pat, .. }) = input { + if let syn::Pat::Ident(ident) = pat.as_ref() { + Some(&ident.ident) + } else { + panic!("Unhandled function argument"); + } + } else { + None + } + }) + .collect::>(); + let ident = &sig.ident; + let field = format_ident!("__mock_{}__", sig.ident); + Some(quote! { + #sig { + match self.#field { + Some(ref func) => func(#(#args),*), + None => self.__mock_fallback__.#ident(#(#args),*), + } + } + }) + } else { + None + } + }) + .collect::>(); + + quote! { + impl #trait_name for #mock_name + where + T: #trait_name, + { + #(#methods)* + } + } + }; + + let builder = quote! { + struct #builder_name + where + T: #trait_name, + { + inner: #mock_name, + } + }; + + let builder_impl = quote! { + impl #builder_name + where + T: #trait_name, + { + #(#builder_methods)* + + fn build(self) -> #mock_name { + self.inner + } + } + }; + // output + input.items.push(TraitItem::Method(mock_method)); + quote! { + #input + + #mock + + #mock_impl + + #builder + + #builder_impl + } +} + +pub(crate) fn mock(_attr: TokenStream, input: TokenStream) -> TokenStream { + let item: Item = syn::parse2(input.into()).unwrap(); + match item { + Item::Trait(item) => process_item_trait(item).into(), + _ => panic!("Traits only supported for mocking"), + } +} \ No newline at end of file diff --git a/macro/src/impls/mod.rs b/macro/src/impls/mod.rs new file mode 100644 index 0000000..60bdb0f --- /dev/null +++ b/macro/src/impls/mod.rs @@ -0,0 +1,227 @@ +#[allow(unused_imports)] +use proc_macro::TokenStream; +use serde::Deserialize; + +mod compose; +pub(crate) use self::compose::*; + +mod contract_aspect; +pub(crate) use self::contract_aspect::*; + +mod decorate; +pub(crate) use self::decorate::*; + +mod delegate; +pub(crate) use self::delegate::*; + +mod enums; +pub(crate) use self::enums::*; + +mod mock; +pub(crate) use self::mock::*; + +mod register; +pub(crate) use self::register::*; + +mod utils; + +#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)] +struct MainConfig { + #[serde(default)] + backtrace: BacktraceConfig, + #[serde(default)] + log: LogConfig, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)] +struct LogConfig { + #[serde(default)] + level: LogLevel, + #[serde(default)] + output: LogOutput, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)] +enum LogOutput { + #[default] + StdOut, + Syslog, + File(String), +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)] +enum LogLevel { + Trace, + Debug, + #[default] + Info, + Warn, + Error, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)] +struct BacktraceConfig { + #[serde(default)] + level: BactraceLevel, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)] +enum BactraceLevel { + None, + #[default] + Short, + Full, +} + +#[cfg(not(test))] // Work around for rust-lang/rust#62127 +pub(crate) fn main(attr: TokenStream, item: TokenStream) -> TokenStream { + let attr = proc_macro2::TokenStream::from(attr); + let config = if !attr.is_empty() { + match serde_tokenstream::from_tokenstream::(&attr) { + Ok(config) => config, + Err(err) => return err.to_compile_error().into(), + } + } else { + MainConfig::default() + }; + + let input: syn::ItemFn = syn::parse2(item.into()).unwrap(); + + let backtrace_level = match config.backtrace.level { + BactraceLevel::None => quote::quote!("0"), + BactraceLevel::Short => quote::quote!("1"), + BactraceLevel::Full => quote::quote!("full"), + }; + + let log_level = match config.log.level { + LogLevel::Trace => quote::quote!("trace"), + LogLevel::Debug => quote::quote!("debug"), + LogLevel::Info => quote::quote!("info"), + LogLevel::Warn => quote::quote!("warn"), + LogLevel::Error => quote::quote!("error"), + }; + + let log_initialization = match config.log.output { + LogOutput::StdOut => { + quote::quote!({ + use logging::{ + colors::{Color, ColoredLevelConfig}, + Dispatch, LevelFilter, + }; + + let colors = ColoredLevelConfig::new() + .debug(Color::Blue) + .info(Color::Green) + .warn(Color::Yellow) + .error(Color::Red) + .trace(Color::BrightBlack); + + Dispatch::new() + .format(move |out, message, record| { + out.finish(format_args!( + "[{} {:5} {}] {}", + format_rfc3339(std::time::SystemTime::now()), + colors.color(record.level()), + record.target(), + message + )) + }) + // .level(LevelFilter::Debug) + .chain(std::io::stdout()) + .apply() + .unwrap(); + }) + } + LogOutput::Syslog => quote::quote!({ + use logging::{Dispatch, LevelFilter}; + + const CARGO_PKG_NAME: &'static str = env!("CARGO_PKG_NAME"); + + let formatter = logging::syslog::Formatter3164 { + facility: logging::syslog::Facility::LOG_USER, + hostname: None, + process: CARGO_PKG_NAME.to_owned(), + pid: 0, + }; + + Dispatch::new() + // Perform allocation-free log formatting + .format(move |out, message, record| { + out.finish(format_args!( + "[{} {:5} {}] {}", + format_rfc3339(std::time::SystemTime::now()), + record.level(), + record.target(), + message + )) + }) + .chain(logging::syslog::unix(formatter).unwrap()) + .apply() + .unwrap(); + }), + LogOutput::File(filename) => quote::quote!({ + use logging::{ + colors::{Color, ColoredLevelConfig}, + Dispatch, LevelFilter, + }; + + Dispatch::new() + // Perform allocation-free log formatting + .format(move |out, message, record| { + out.finish(format_args!( + "[{} {:5} {}] {}", + format_rfc3339(std::time::SystemTime::now()), + record.level(), + record.target(), + message + )) + }) + .chain(logging::log_file(#filename).unwrap()) + .apply() + .unwrap(); + }), + }; + + let syn::ItemFn { + attrs, + vis, + sig, + block, + } = input; + + let stmts = &block.stmts; + let output = quote::quote! { + #(#attrs)* #vis #sig { + if cfg!(debug_assertions) { + use std::env; + + if env::var("RUST_BACKTRACE").is_err() { + env::set_var("RUST_BACKTRACE", #backtrace_level); + } + + if env::var("RUST_LOG").is_err() { + env::set_var("RUST_LOG", #log_level); + } + + panic::backtrace::install(); + } else { + // Build the metadata from the build environment + const PACKAGE_METADATA: panic::PackageMetadata<'static> = panic::PackageMetadata { + pkg_name: env!("CARGO_PKG_NAME"), + crate_name: env!("CARGO_CRATE_NAME"), + version: env!("CARGO_PKG_VERSION"), + repository: env!("CARGO_PKG_REPOSITORY"), + }; + + // Append the new panic handler + panic::handler(panic::suggest_issue_tracker, PACKAGE_METADATA); + } + + #log_initialization + + #(#stmts)* + } + }; + + output.into() +} diff --git a/macro/src/impls/register.rs b/macro/src/impls/register.rs new file mode 100644 index 0000000..bbe39dd --- /dev/null +++ b/macro/src/impls/register.rs @@ -0,0 +1,501 @@ +use std::{ + collections::{HashMap, VecDeque}, + net::UdpSocket, +}; + +use companion::{companion_addr, Response, Task}; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::{quote, ToTokens}; +use syn::{ + visit_mut::VisitMut, Attribute, FnArg, Ident, Item, ItemTrait, Pat, Path, TraitItem, + TraitItemMethod, +}; + +use crate::MAX_UDP_PAYLOAD; + +use super::{AspectJointPoint, ContractAspectState, Enum, SyntaxAndDocs, Type}; + +/// Name used for the "re-routed" method. +fn trait_method_impl_name(name: &str) -> String { + format!("__impl_trait_{}", name) +} + +fn method_rename(method: &TraitItemMethod) -> TraitItemMethod { + let mut method: TraitItemMethod = (*method).clone(); + let name = trait_method_impl_name(&method.sig.ident.to_string()); + + let mut attrs = vec![]; + attrs.push(syn::parse_quote!(#[doc(hidden)])); + attrs.push(syn::parse_quote!(#[doc = " This is an internal function that is not meant to be used directly!"])); + attrs + .push(syn::parse_quote!(#[doc = " See the documentation of the `#[register]` attribute."])); + + // add all existing non-contract attributes + attrs.extend( + method + .attrs + .iter() + .filter(|attr| { + let name = attr.path.segments.last().unwrap().ident.to_string(); + + Type::type_and_mode(&name).is_none() && name != "aspect" && name != "doc" + }) + .cloned(), + ); + + method.attrs = attrs; + method.sig.ident = Ident::new(&name, method.sig.ident.span()); + + method +} + +struct ArgInfo { + call_toks: proc_macro2::TokenStream, +} + +// Calculate name and pattern tokens +fn arg_pat_info(pat: &Pat) -> ArgInfo { + match pat { + Pat::Ident(ident) => { + let toks = quote::quote! { + #ident + }; + ArgInfo { call_toks: toks } + } + Pat::Tuple(tup) => { + let infos = tup.elems.iter().map(arg_pat_info); + + let toks = { + let mut toks = proc_macro2::TokenStream::new(); + + for info in infos { + toks.extend(info.call_toks); + toks.extend(quote::quote!(,)); + } + + toks + }; + + ArgInfo { + call_toks: quote::quote!((#toks)), + } + } + Pat::TupleStruct(_tup) => unimplemented!(), + p => panic!("Unsupported pattern type: {:?}", p), + } +} + +#[allow(unused_variables)] +fn process_item_trait(path: String, mut input: ItemTrait) -> TokenStream2 { + // create method wrappers and renamed items + let funcs = input + .items + .iter() + .filter_map(|item| { + if let TraitItem::Method(method) = item { + let rename = method_rename(method); + let wrapper = { + // create method wrapper + let mut method = (*method).clone(); + let args = method + .sig + .inputs + .clone() + .into_iter() + .map(|arg| { + // + match &arg { + FnArg::Receiver(_) => quote! {self}, + FnArg::Typed(p) => { + let info = arg_pat_info(&p.pat); + + info.call_toks + } + } + }) + .collect::>(); + let arguments = { + let mut toks = proc_macro2::TokenStream::new(); + + for arg in args { + toks.extend(arg); + toks.extend(quote::quote!(,)); + } + + toks + }; + + // pre-process here + let body: TokenStream2 = { + let name = trait_method_impl_name(&method.sig.ident.to_string()); + let name = syn::Ident::new(&name, method.sig.ident.span()); + + quote::quote! { + { + Self::#name(#arguments) + } + } + }; + + let mut attrs = vec![]; + + // keep the documentation and contracts of the original method + attrs.extend( + method + .attrs + .iter() + .filter(|attr| { + let name = attr.path.segments.last().unwrap().ident.to_string(); + Type::type_and_mode(&name).is_some() + || name == "aspect" + || name == "doc" + }) + .cloned(), + ); + // always inline + attrs.push(syn::parse_quote!(#[inline(always)])); + + // INFO: NEW PROCESS + let mut state: ContractAspectState = ContractAspectState::default(); + let attrs = state.process(&attrs); + + let variables = state.variables(); + + let mut contract_docs: VecDeque = VecDeque::new(); + + let (requires, docs) = state.requires.generate(); + contract_docs.extend(docs); + + let (invariants, docs) = state.invariants.generate(); + contract_docs.extend(docs); + + let (ensures, docs) = state.ensures.generate(); + contract_docs.extend(docs); + + if !contract_docs.is_empty() { + contract_docs.push_front(String::from(" # Contract")); + } + + let before = state + .aspects + .iter() + .filter_map(|aspect| { + if let Some(item) = &aspect.before { + item.default.as_ref().map(|block| { + let stmts = &block.stmts; + quote! { + #(#stmts)* + } + }) + } else { + None + } + }) + .collect::>(); + + let after = state + .aspects + .iter() + .rev() + .filter_map(|aspect| { + if let Some(item) = &aspect.after { + item.default.as_ref().map(|block| { + let stmts = &block.stmts; + quote! { + #(#stmts)* + } + }) + } else { + None + } + }) + .collect::>(); + + // Here we should deal with call name ))) + // For traits we need only change some in output + // let mut around = quote! {inner()}; // body + let mut around = body.clone(); // body + let callee = body.clone(); + let mut has_around = false; + + let mut it = state.aspects.iter().rev().peekable(); + while let Some(aspect) = it.next() { + if let Some(item) = &aspect.around { + item.default.as_ref().map(|block| { + let mut replacer = AspectJointPoint { stream: &around }; + + let mut stmts = block.stmts.clone(); + + for stmt in stmts.iter_mut() { + replacer.visit_stmt_mut(stmt); + } + + if it.peek().is_none() { + around = quote!(#(#stmts)*); + } else { + around = quote!({#(#stmts)*}); + } + has_around = true; + }); + } + } + + let attrs = { + let mut new_attrs: Vec = Vec::new(); + + if !state.docs.is_empty() { + let mut it = state.docs.iter().peekable(); + while let Some(comment) = it.next() { + if it.peek().is_some() { + new_attrs.push(syn::parse_quote!(#[doc = #comment])); + } else if !comment.trim().is_empty() { + new_attrs.push(syn::parse_quote!(#[doc = #comment])); + } + } + new_attrs.push(syn::parse_quote!(#[doc = ""])); + } + + if !contract_docs.is_empty() { + let mut it = contract_docs.iter().peekable(); + while let Some(comment) = it.next() { + if it.peek().is_some() { + new_attrs.push(syn::parse_quote!(#[doc = #comment])); + } else if !comment.trim().is_empty() { + new_attrs.push(syn::parse_quote!(#[doc = #comment])); + } + } + new_attrs.push(syn::parse_quote!(#[doc = ""])); + } + + let mut aspect_docs: VecDeque = VecDeque::new(); + // INFO: Aspects + state.aspects.iter().for_each(|aspect| { + // + let mut docs = aspect.documentation(); + if !docs.is_empty() { + docs.push_back(String::new()); + aspect_docs.extend(docs); + } + }); + + if !aspect_docs.is_empty() { + aspect_docs.push_front(String::from(" # Aspects")); + let mut it = aspect_docs.iter().peekable(); + while let Some(comment) = it.next() { + if it.peek().is_some() { + new_attrs.push(syn::parse_quote!(#[doc = #comment])); + } else if !comment.trim().is_empty() { + new_attrs.push(syn::parse_quote!(#[doc = #comment])); + } + } + new_attrs.push(syn::parse_quote!(#[doc = ""])); + } + attrs.iter().cloned().for_each(|attr| { + new_attrs.push(attr); + }); + new_attrs + }; + + // INFO: END OF NEW PROCESS + method.attrs = attrs; + + { + let result = if has_around { + quote! { + let result = #callee; + } + } else { + quote! { + let result = { + #around + }; + } + }; + let body = quote! { + { + #(#requires)* + #(#invariants)* + + #(#variables)* + + #(#before)* + + #result + + #(#after)* + + #(#invariants)* + #(#ensures)* + + result + } + }; + let block: syn::Block = syn::parse2(body).unwrap(); + method.default = Some(block); + method.semi_token = None; + } + + method + }; + + Some(vec![TraitItem::Method(rename), TraitItem::Method(wrapper)]) + } else { + None + } + }) + .flatten() + .collect::>(); + + // remove all previous methods + input.items = input + .items + .into_iter() + .filter(|item| { + // + match item { + TraitItem::Method(_) => false, + _ => true, + } + }) + .collect(); + + // add back new methods + input.items.extend(funcs); + + let _ = process_remote_trait(path, input.clone()); + + input.into_token_stream() +} + +// Remote processing +// Cutoff all attributes +fn process_remote_trait(path: String, mut input: ItemTrait) -> TokenStream2 { + // Cutoff remote attributes + input.attrs = vec![]; + input.items.iter_mut().for_each(|item| { + // Cutoff every remote methods attributes and body + if let TraitItem::Method(method) = item { + method.attrs = vec![]; + + if method.default.is_some() { + method.default = None; + method.semi_token = Some(syn::token::Semi(Span::call_site())); + } + } + }); + let data = syn_serde::json::to_string(&input); + + let mut buf = [0; MAX_UDP_PAYLOAD]; + + let addr = companion_addr(); + + let socket = UdpSocket::bind("[::]:0").unwrap(); + socket.connect(addr).unwrap(); + + socket.send(&Task::Set(&path, &data).as_bytes()).unwrap(); + let (len, _src) = socket.recv_from(&mut buf).unwrap(); + let _resp = Response::from(&buf[..len]); + + TokenStream2::new() +} + +fn process_enum_trait(path: String, input: ItemTrait) -> TokenStream2 { + let supertraits = input + .supertraits + .iter() + .filter_map(|item| match item { + syn::TypeParamBound::Trait(item) => { + // + Some( + item.path + .segments + .iter() + .map(|segment| segment.ident.to_string()) + .collect::>() + .join("::"), + ) + } + syn::TypeParamBound::Lifetime(_) => None, + }) + .collect::>(); + + let item = Enum { + supertraits, + items: HashMap::new(), + }; + + let data = serde_json::to_string(&item).unwrap(); + + let mut buf = [0; MAX_UDP_PAYLOAD]; + + let addr = companion_addr(); + + let socket = UdpSocket::bind("[::]:0").unwrap(); + socket.connect(addr).unwrap(); + + socket.send(&Task::Set(&path, &data).as_bytes()).unwrap(); + let (len, _src) = socket.recv_from(&mut buf).unwrap(); + let _resp = Response::from(&buf[..len]); + + TokenStream2::new() +} + +fn process_aspect_trait(path: String, input: ItemTrait) -> TokenStream2 { + input.items.iter().for_each(|item| { + // check input + match item { + TraitItem::Method(method) => { + let ident = method.sig.ident.to_string(); + match ident.as_str() { + "before" | "after" | "around" => {} + _ => { + panic!("Aspect supports only `before`, `after` and `around` methods") + } + } + } + _ => panic!("Aspect definition support only methods"), + } + }); + let data = syn_serde::json::to_string(&input); + + let mut buf = [0; MAX_UDP_PAYLOAD]; + + let addr = companion_addr(); + + let socket = UdpSocket::bind("[::]:0").unwrap(); + socket.connect(addr).unwrap(); + + socket.send(&Task::Set(&path, &data).as_bytes()).unwrap(); + let (len, _src) = socket.recv_from(&mut buf).unwrap(); + let _resp = Response::from(&buf[..len]); + + input.into_token_stream() +} + +pub(crate) fn register(attrs: TokenStream2, input: TokenStream2) -> TokenStream2 { + let path: Path = syn::parse2(attrs).unwrap(); + let path = path + .segments + .iter() + .map(|item| item.ident.to_string()) + .collect::>() + .join("::"); + + let item: Item = syn::parse2(input).unwrap(); + match item { + Item::Trait(item) => { + // + let ident = item.ident.to_string(); + if ident.ends_with("Aspect") { + process_aspect_trait(path, item) + } else if ident.ends_with("Remote") { + process_remote_trait(path, item) + } else if ident.ends_with("Enum") { + process_enum_trait(path, item) + } else { + process_item_trait(path, item) + } + } + _ => panic!("Traits only supported for registration"), + } +} diff --git a/macro/src/impls/utils.rs b/macro/src/impls/utils.rs new file mode 100644 index 0000000..f40f3c1 --- /dev/null +++ b/macro/src/impls/utils.rs @@ -0,0 +1,49 @@ +#![allow(dead_code, unused_variables)] +use syn::{ + parse::{Parse, ParseStream, Result}, + punctuated::Punctuated, + Ident, Token, TypePath, +}; + +pub(crate) const MAX_UDP_PAYLOAD: usize = 65507; + +pub struct AdviceField { + member: Ident, + // colon: Token![:], + // value: syn::LitStr, + value: TypePath, +} + +impl Parse for AdviceField { + fn parse(input: ParseStream) -> Result { + let member: Ident = input.parse()?; + let colon_token: Token![:] = input.parse()?; + // let value: Expr = input.parse()?; + // let value: syn::LitStr = input.parse()?; + let value: TypePath = input.parse()?; + + Ok(AdviceField { + member, + // colon_token, + value, + }) + } +} + +pub struct Args { + // advice: Ident, + // before: Ident, + // after: Ident, +} + +impl Parse for Args { + fn parse(input: ParseStream) -> Result { + let vars = Punctuated::::parse_terminated(input)?; + let idents: Vec = vars.into_iter().collect(); + // dbg!(idents); + // todo!("GOOD") + Ok(Args { + // vars: vars.into_iter().collect(), + }) + } +} diff --git a/macro/src/lib.rs b/macro/src/lib.rs index 20ab885..bedc21f 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -1,68 +1,432 @@ -#![allow(unused_imports, unused_variables, dead_code, non_snake_case)] +// #![feature(proc_macro_span)] +#![allow(unused_imports)] +#![allow(clippy::needless_doctest_main)] +// #![warn( +// missing_debug_implementations, +// missing_docs, +// rust_2018_idioms, +// unreachable_pub +// )] +#![doc(test( + no_crate_inject, + attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables)) +))] -use proc_macro::TokenStream; -use quote::quote; -use syn::{ - parse::{Parse, ParseStream, Result}, - parse_macro_input, - punctuated::Punctuated, - DeriveInput, Ident, ItemFn, Token, -}; +use proc_macro::{Span, TokenStream, TokenTree}; -struct AdviceField { - member: Ident, - // colon: Token![:], - value: syn::LitStr, +mod impls; +use impls::{ItemLocation, Mode, Type}; + +const MAX_UDP_PAYLOAD: usize = 65507; + +/// Composition root for application entry point. +/// +/// Inspired by `#[tokio::main]` +/// +/// Implements dependency injection composition root pattern for +/// application entry point. +/// +/// ## Examples +/// +/// ``` +/// #[ruex::main { +/// backtrace = { +/// level = Short, +/// }, +/// log = { +/// level = Info, +/// output = Stdout, +/// } +/// }] +/// fn main() {} +/// ``` +/// +/// ## Config +/// Configuration fields can be omitted. +/// +/// ```rust,no_run +/// struct MainConfig { +/// backtrace: BacktraceConfig, +/// log: LogConfig, +/// } +/// +/// struct LogConfig { +/// level: LogLevel, +/// output: LogOutput, +/// } +/// +/// enum LogOutput { +/// #[default] +/// StdOut, +/// Syslog, +/// File(String), +/// } +/// +/// enum LogLevel { +/// Trace, +/// Debug, +/// #[default] +/// Info, +/// Warn, +/// Error, +/// } +/// +/// struct BacktraceConfig { +/// level: BactraceLevel, +/// } +/// +/// enum BactraceLevel { +/// None, +/// #[default] +/// Short, +/// Full, +/// } +/// ``` +#[proc_macro_attribute] +#[cfg(not(test))] // Work around for rust-lang/rust#62127 +pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream { + impls::main(attr, item) +} + +/// Decorate methods. +/// +/// Inspired by `deco` +/// +/// ## Examples +/// +/// ``` +/// use ruex::prelude::*; +/// +/// fn logging(func: F) -> impl Fn(i32) -> i32 +/// where +/// F: Fn(i32) -> i32, +/// { +/// move |i| { +/// println!("Input = {}", i); +/// let out = func(i); +/// println!("Output = {}", out); +/// out +/// } +/// } +/// +/// #[decorate(logging)] +/// fn add2(i: i32) -> i32 { +/// i + 2 +/// } +/// +/// add2(2); +/// ``` +/// +/// - Decorator with parameter +/// +/// ``` +/// use ruex::prelude::*; +/// use std::{fs, io::Write}; +/// +/// fn logging( +/// log_filename: &'static str, +/// ) -> impl Fn(InputFunc) -> Box i32> +/// where +/// InputFunc: Fn(i32) -> i32, +/// { +/// move |func: InputFunc| { +/// Box::new(move |i: i32| { +/// let mut f = fs::File::create(log_filename).unwrap(); +/// writeln!(f, "Input = {}", i).unwrap(); +/// let out = func(i); +/// writeln!(f, "Output = {}", out).unwrap(); +/// out +/// }) +/// } +/// } +/// +/// #[decorate(logging("test.log"))] +/// fn add2(i: i32) -> i32 { +/// i + 2 +/// } +/// +/// add2(2); +/// ``` +/// +#[proc_macro_attribute] +pub fn decorate(attr: TokenStream, item: TokenStream) -> TokenStream { + impls::decorate(attr, item) +} + +/// Delegate method to a field. +/// +/// Inspired by delegate-attr +/// +/// ## Examples +/// +/// ### Delegate `impl` block +/// +/// ``` +/// use delegate_attr::delegate; +/// +/// struct Foo(String); +/// +/// #[delegate(self.0)] +/// impl Foo { +/// fn as_str(&self) -> &str; +/// fn into_bytes(self) -> Vec; +/// } +/// +/// let foo = Foo("hello".to_owned()); +/// assert_eq!(foo.as_str(), "hello"); +/// assert_eq!(foo.into_bytes(), b"hello"); +/// ``` +/// +/// ### Delegate trait `impl` +/// +/// ``` +/// # use delegate_attr::delegate; +/// +/// struct Iter(std::vec::IntoIter); +/// +/// #[delegate(self.0)] +/// impl Iterator for Iter { +/// type Item = u8; +/// fn next(&mut self) -> Option; +/// fn count(self) -> usize; +/// fn size_hint(&self) -> (usize, Option); +/// fn last(self) -> Option; +/// } +/// +/// let iter = Iter(vec![1, 2, 4, 8].into_iter()); +/// assert_eq!(iter.count(), 4); +/// let iter = Iter(vec![1, 2, 4, 8].into_iter()); +/// assert_eq!(iter.last(), Some(8)); +/// let iter = Iter(vec![1, 2, 4, 8].into_iter()); +/// assert_eq!(iter.sum::(), 15); +/// ``` +/// +/// ### With more complicated target +/// +/// ``` +/// # use delegate_attr::delegate; +/// # use std::cell::RefCell; +/// struct Foo { +/// inner: RefCell>, +/// } +/// +/// #[delegate(self.inner.borrow())] +/// impl Foo { +/// fn len(&self) -> usize; +/// } +/// +/// #[delegate(self.inner.borrow_mut())] +/// impl Foo { +/// fn push(&self, value: T); +/// } +/// +/// #[delegate(self.inner.into_inner())] +/// impl Foo { +/// fn into_boxed_slice(self) -> Box<[T]>; +/// } +/// +/// let foo = Foo { inner: RefCell::new(vec![1]) }; +/// assert_eq!(foo.len(), 1); +/// foo.push(2); +/// assert_eq!(foo.len(), 2); +/// assert_eq!(foo.into_boxed_slice().as_ref(), &[1, 2]); +/// ``` +/// +/// ### `into` and `call` attribute +/// +/// ``` +/// # use delegate_attr::delegate; +/// struct Inner; +/// impl Inner { +/// pub fn method(&self, num: u32) -> u32 { num } +/// } +/// +/// struct Wrapper { inner: Inner } +/// +/// #[delegate(self.inner)] +/// impl Wrapper { +/// // calls method, converts result to u64 +/// #[into] +/// pub fn method(&self, num: u32) -> u64; +/// +/// // calls method, returns () +/// #[call(method)] +/// pub fn method_noreturn(&self, num: u32); +/// } +/// ``` +/// +/// ### Delegate single method +/// +/// ``` +/// # use delegate_attr::delegate; +/// struct Foo(Vec); +/// +/// impl Foo { +/// #[delegate(self.0)] +/// fn len(&self) -> usize; +/// } +/// +/// let foo = Foo(vec![1]); +/// assert_eq!(foo.len(), 1); +/// ``` +#[proc_macro_attribute] +pub fn delegate(attr: TokenStream, item: TokenStream) -> TokenStream { + impls::delegate(attr, item) +} + +/// Elegant trait mocking. +/// +/// Use it with #[cfg_attr(test, mock)] +/// +/// ## Examples +/// +/// ```rust +/// #![allow(unused_variables)] +/// use ruex::prelude::*; +/// +/// /// Here +/// #[mock] +/// trait Nurse { +/// fn heal(&self, value: i32, direction: i32) -> i32 { +/// 0 +/// } +/// +/// fn leave(&self, value: i32) -> i32 { +/// 0 +/// } +/// } +/// +/// #[derive(Default)] +/// struct Foo; +/// +/// impl Nurse for Foo { +/// fn heal(&self, value: i32, direction: i32) -> i32 { +/// 25 +/// } +/// +/// fn leave(&self, value: i32) -> i32 { +/// 31 +/// } +/// } +/// +/// fn main() { +/// let nurse = Foo::mock().heal(|value, direction| 123).build(); +/// +/// let val = nurse.heal(23, 0); +/// println!("VALUE: {val}"); +/// +/// let val = nurse.leave(23); +/// println!("VALUE: {val}"); +/// } +/// ``` +#[proc_macro_attribute] +pub fn mock(attr: TokenStream, item: TokenStream) -> TokenStream { + impls::mock(attr, item) +} + +/// Subject registration. +#[proc_macro_attribute] +pub fn register(attr: TokenStream, item: TokenStream) -> TokenStream { + impls::register(attr.into(), item.into()).into() +} + +/// Subject mounting. +#[proc_macro_attribute] +pub fn mount(attr: TokenStream, item: TokenStream) -> TokenStream { + // let mut it = item.clone().into_iter(); + // let first = it.next().unwrap(); + // let last = it.last().unwrap(); + + // let path = first + // .span() + // .source_file() + // .path() + // .to_str() + // .unwrap() + // .to_string(); + // let range = (first.span().start().line, last.span().end().line); + let path = String::new(); + let pos = ItemLocation { + path, + range: (0, 0), + }; + + impls::mount(attr.into(), item.into(), pos).into() } -impl Parse for AdviceField { - fn parse(input: ParseStream) -> Result { - let member: Ident = input.parse()?; - let colon_token: Token![:] = input.parse()?; - // let value: Expr = input.parse()?; - let value: syn::LitStr = input.parse()?; +/// Aspect-oriented methodology. +/// +#[proc_macro_attribute] +pub fn aspect(attr: TokenStream, item: TokenStream) -> TokenStream { + impls::contracts_aspect(Type::Aspect, Mode::Always, attr.into(), item.into()).into() +} - Ok(AdviceField { - member, - // colon_token, - value, - }) - } +/// Composition pattern implementation. +/// +#[proc_macro_derive(Compose, attributes(delegate))] +pub fn derive_compose(item: TokenStream) -> TokenStream { + impls::compose(item.into()).into() } -struct Args { - // advice: Ident, - // before: Ident, - // after: Ident, +/// Debug ensures for contracts. +/// +#[proc_macro_attribute] +pub fn debug_ensures(attr: TokenStream, item: TokenStream) -> TokenStream { + impls::contracts_aspect(Type::Ensures, Mode::Debug, attr.into(), item.into()).into() } -impl Parse for Args { - fn parse(input: ParseStream) -> Result { - let vars = Punctuated::::parse_terminated(input)?; - let idents: Vec = vars.into_iter().collect(); - // dbg!(idents); - // todo!("GOOD") - Ok(Args { - // vars: vars.into_iter().collect(), - }) - } +/// Debug invariant for contracts. +/// +#[proc_macro_attribute] +pub fn debug_invariant(attr: TokenStream, item: TokenStream) -> TokenStream { + impls::contracts_aspect(Type::Invariant, Mode::Debug, attr.into(), item.into()).into() } +/// Debug requires for contracts. +/// #[proc_macro_attribute] -pub fn Aspect(attr: TokenStream, item: TokenStream) -> TokenStream { - // println!("attr: \"{}\"", attr.to_string()); - let attr = attr.clone(); - let attr = syn::parse_macro_input!(attr as Args); +pub fn debug_requires(attr: TokenStream, item: TokenStream) -> TokenStream { + impls::contracts_aspect(Type::Requires, Mode::Debug, attr.into(), item.into()).into() +} - // println!("item: \"{}\"", item.to_string()); +/// Ensures for contracts. +/// +#[proc_macro_attribute] +pub fn ensures(attr: TokenStream, item: TokenStream) -> TokenStream { + impls::contracts_aspect(Type::Ensures, Mode::Always, attr.into(), item.into()).into() +} - item +/// Invariant for contracts. +/// +#[proc_macro_attribute] +pub fn invariant(attr: TokenStream, item: TokenStream) -> TokenStream { + impls::contracts_aspect(Type::Invariant, Mode::Always, attr.into(), item.into()).into() } -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } +/// Requires for contracts. +/// +#[proc_macro_attribute] +pub fn requires(attr: TokenStream, item: TokenStream) -> TokenStream { + impls::contracts_aspect(Type::Requires, Mode::Always, attr.into(), item.into()).into() +} + +/// Test ensures for contracts. +/// +#[proc_macro_attribute] +pub fn test_ensures(attr: TokenStream, item: TokenStream) -> TokenStream { + impls::contracts_aspect(Type::Ensures, Mode::Test, attr.into(), item.into()).into() +} + +/// Test invariant for contracts. +/// +#[proc_macro_attribute] +pub fn test_invariant(attr: TokenStream, item: TokenStream) -> TokenStream { + impls::contracts_aspect(Type::Invariant, Mode::Test, attr.into(), item.into()).into() +} + +/// Test requires for contracts. +/// +#[proc_macro_attribute] +pub fn test_requires(attr: TokenStream, item: TokenStream) -> TokenStream { + impls::contracts_aspect(Type::Requires, Mode::Test, attr.into(), item.into()).into() } diff --git a/src/foundation/patterns/default/controller.rs b/src/foundation/patterns/default/controller.rs index 54d7af8..a842632 100644 --- a/src/foundation/patterns/default/controller.rs +++ b/src/foundation/patterns/default/controller.rs @@ -86,10 +86,10 @@ where log::info!("Execute Command [BaseController] {:?}", notification); - command_map.get(¬ification.interest()).map(|command| { + if let Some(command) = command_map.get(¬ification.interest()) { log::info!("Command [BaseController] {:?} for {:?}", command, notification); command.execute(notification) - }); + } } fn has_command(&self, interest: &Interest) -> bool { diff --git a/src/foundation/patterns/default/model.rs b/src/foundation/patterns/default/model.rs index 2ee5847..f738df3 100644 --- a/src/foundation/patterns/default/model.rs +++ b/src/foundation/patterns/default/model.rs @@ -24,6 +24,9 @@ use crate::prelude::{Model, Proxy, Singleton}; /// /// [Command]: crate::prelude::Command /// [Facade]: crate::prelude::Facade +/// + +#[derive(Default)] pub struct BaseModel { // Mapping of proxy types to [Proxy] instances storages: RefCell>>, diff --git a/src/foundation/patterns/default/view.rs b/src/foundation/patterns/default/view.rs index 39fbac0..ae52d63 100644 --- a/src/foundation/patterns/default/view.rs +++ b/src/foundation/patterns/default/view.rs @@ -8,7 +8,10 @@ use std::{ use crate::{ foundation::patterns::observer::BaseObserver, - prelude::{Interest, Mediator, MediatorRegistry, Notification, NotifyContext, Observer, Singleton, View}, + prelude::{ + Interest, Mediator, MediatorRegistry, Notification, NotifyContext, Observer, Singleton, + View, + }, }; /// A Singleton [View] implementation. @@ -81,12 +84,7 @@ where // Copy observers from reference array to working array, // since the reference array may change during the notification loop // and prevent double borrow )) - let observers = { - self.observer_map - .borrow() - .get(¬e.interest()) - .map(|observers| observers.clone()) - }; + let observers = self.observer_map.borrow().get(¬e.interest()).cloned(); if let Some(observers) = observers { for observer in observers.iter() { @@ -100,13 +98,11 @@ where // log::info!("Register Observer [BaseView] {:?}", interest); let mut observer_map = self.observer_map.borrow_mut(); - if !observer_map.contains_key(&interest) { - observer_map.insert(interest.clone(), Vec::new()); - } + observer_map.entry(interest).or_insert_with(Vec::new); - observer_map - .get_mut(&interest) - .map(|observers| observers.push(observer)); + if let Some(observers) = observer_map.get_mut(&interest) { + observers.push(observer) + } } // It private so its fun @@ -114,17 +110,17 @@ where let mut observer_map = self.observer_map.borrow_mut(); // the observer list for the notification under inspection - observer_map.remove(interest).as_mut().map(|observers| { + if let Some(observers) = observer_map.remove(interest).as_mut() { // find the observer for the notify_context for (idx, observer) in observers.iter().enumerate() { - if observer.compare_context(context) == true { + if observer.compare_context(context) { // there can only be one Observer for a given notify_context // in any given Observer list, so remove it and break observers.remove(idx); break; } } - }); + } } } @@ -148,7 +144,7 @@ where // Get Notification interests, if any. let interests = mediator.list_notification_interests(); - if interests.len() > 0 { + if !interests.is_empty() { let mediator = mediator.clone(); let context = mediator.clone(); // Create Observer @@ -162,7 +158,7 @@ where // Register Mediator as Observer for its list of Notification interests for interest in interests.iter() { - self.register_observer(interest.clone(), observer.clone()); + self.register_observer(*interest, observer.clone()); } } @@ -174,7 +170,7 @@ where match self.mediator_map.borrow().get(&type_id) { Some(item) => match item.clone().downcast::() { - Ok(mediator) => Some(mediator.clone()), + Ok(mediator) => Some(mediator), Err(_) => { log::error!("Something wrong with proxy storage"); None @@ -188,42 +184,45 @@ where // remove the mediator from the map let type_id = TypeId::of::(); - self.mediator_map.borrow_mut().remove(&type_id).map(|mediator| { - match mediator.downcast::() { - Ok(mediator) => { - // for every notification this mediator is interested in... - let interests = mediator.list_notification_interests(); - for interest in interests.iter() { - // remove the observer linking the mediator - // to the notification interest - - let mut observer_map = self.observer_map.borrow_mut(); - - let context = mediator.id(); - - // the observer list for the notification under inspection - observer_map.remove(interest).as_mut().map(|observers| { - // find the observer for the notify_context - for (idx, observer) in observers.iter().enumerate() { - if observer.context().id() == context { - // there can only be one Observer for a given notify_context - // in any given Observer list, so remove it and break - observers.remove(idx); - break; + self.mediator_map + .borrow_mut() + .remove(&type_id) + .map(|mediator| { + match mediator.downcast::() { + Ok(mediator) => { + // for every notification this mediator is interested in... + let interests = mediator.list_notification_interests(); + for interest in interests.iter() { + // remove the observer linking the mediator + // to the notification interest + + let mut observer_map = self.observer_map.borrow_mut(); + + let context = mediator.id(); + + // the observer list for the notification under inspection + if let Some(observers) = observer_map.remove(interest).as_mut() { + // find the observer for the notify_context + for (idx, observer) in observers.iter().enumerate() { + if observer.context().id() == context { + // there can only be one Observer for a given notify_context + // in any given Observer list, so remove it and break + observers.remove(idx); + break; + } } } - }); - } + } - // alert the mediator that it has been removed - mediator.on_remove(); - mediator - } - Err(_) => { - panic!("Something wrong with mediator storage"); + // alert the mediator that it has been removed + mediator.on_remove(); + mediator + } + Err(_) => { + panic!("Something wrong with mediator storage"); + } } - } - }) + }) } fn has_mediator>(&self) -> bool { diff --git a/src/foundation/patterns/fsm/integrations.rs b/src/foundation/patterns/fsm/integrations.rs index 49f886b..29bfbcf 100644 --- a/src/foundation/patterns/fsm/integrations.rs +++ b/src/foundation/patterns/fsm/integrations.rs @@ -18,11 +18,17 @@ pub struct CallbackIntegration; impl CallbackIntegration { /// Create new callback integration - pub fn new() {} + pub fn new() -> Self { + Self + } } impl FsmIntegration for CallbackIntegration { - fn transition(&self, new_state: Rc>, old_state: Option>>) -> bool { + fn transition( + &self, + new_state: Rc>, + old_state: Option>>, + ) -> bool { if let Some(ref old_state) = old_state { old_state.state.exit(self) } diff --git a/src/foundation/patterns/mediator/mediator.rs b/src/foundation/patterns/mediator/mediator.rs index 40f51f5..a91feb3 100644 --- a/src/foundation/patterns/mediator/mediator.rs +++ b/src/foundation/patterns/mediator/mediator.rs @@ -30,7 +30,7 @@ where Body: fmt::Debug + 'static, { fn view_component(&self) -> Option>> { - self.view_component.as_ref().map(|c| c.clone()) + self.view_component.as_ref().cloned() } fn handle_notification(&self, _notification: Rc>) {} diff --git a/src/foundation/patterns/mod.rs b/src/foundation/patterns/mod.rs index a222283..c29257c 100644 --- a/src/foundation/patterns/mod.rs +++ b/src/foundation/patterns/mod.rs @@ -1,5 +1,11 @@ //! Catalog of patterns +#![allow( + clippy::module_inception, + clippy::new_without_default, + clippy::type_complexity +)] + pub mod command; pub mod default; @@ -14,4 +20,4 @@ pub mod observer; pub mod proxy; -pub mod builder; \ No newline at end of file +pub mod builder; diff --git a/src/foundation/patterns/observer/notifier.rs b/src/foundation/patterns/observer/notifier.rs index d90d8ae..7d2bdf0 100644 --- a/src/foundation/patterns/observer/notifier.rs +++ b/src/foundation/patterns/observer/notifier.rs @@ -28,6 +28,7 @@ use crate::{ /// [Proxy]: crate::prelude::Proxy /// [Facade]: crate::prelude::Facade +#[derive(Default)] pub struct BaseNotifier; impl BaseNotifier { diff --git a/src/lib.rs b/src/lib.rs index 2300b36..9e0806c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,34 +1,37 @@ #![doc(html_logo_url = "https://dudochkin-victor.github.io/assets/ruex/logo.svg")] - #![warn(missing_docs)] -//! Design pattern framework on top of PureMVC. -//! +//! Design pattern framework on top of PureMVC. +//! //! The PureMVC framework has a very narrow goal. That is to help you //! separate your application’s coding interests into three discrete tiers: //! [Model][2], [View][3] and [Controller][1]. -//! +//! //! This separation of interests, and the tightness and direction of the //! couplings used to make them work together is of paramount //! importance in the building of scalable and maintainable applications. -//! +//! //! In this implementation of the classic MVC Design meta-pattern, these //! three tiers of the application are governed by three Singletons (a class //! where only one instance may be created) called simply [Model][2], [View][3] //! and [Controller][1]. Together, they are referred to as the ‘Core actors’. -//! +//! //! A fourth Singleton, the [Facade][4] simplifies development by providing a //! single interface for communication with the Core actors. -//! +//! //! [Read more..][foundation] -//! +//! //! ![PureMVC Diagram](https://raw.githubusercontent.com/wiki/ohyo-io/wampire/images/pure-mvc.svg) -//! +//! //! [1]: crate::prelude::Controller //! [2]: crate::prelude::Model //! [3]: crate::prelude::View //! [4]: crate::prelude::Facade -//! +//! pub mod foundation; pub mod prelude; + +pub mod utils; + +pub use ruex_macro::main; diff --git a/src/prelude/compose.rs b/src/prelude/compose.rs new file mode 100644 index 0000000..08f2640 --- /dev/null +++ b/src/prelude/compose.rs @@ -0,0 +1,21 @@ +/// Function composition +/// +/// Allows to compose functions and closures. +/// +pub trait Compose: Fn(In) -> Out { + /// `impl Trait` only allowed in function and inherent method return types, + /// not in trait method return - so its boxed + fn chain(self, next: impl Fn(Out) -> Ret + 'static) -> Box Ret>; +} + +impl Compose for F +where + F: Fn(In) -> Out + 'static, +{ + fn chain(self, next: impl Fn(Out) -> Ret + 'static) -> Box Ret> { + Box::new(move |args: In| { + // chained + next(self(args)) + }) + } +} diff --git a/src/prelude/currying.rs b/src/prelude/currying.rs new file mode 100644 index 0000000..08b0cfa --- /dev/null +++ b/src/prelude/currying.rs @@ -0,0 +1,118 @@ +//! Functions with lots of parameters are considered bad style and +//! reduce readability (“what does the 5th parameter mean?”). +//! Consider grouping some parameters into a new type. + +/// Currying for functions with 2 params +/// +pub trait Curry { + /// The concrete type that `curry` returns. + type Output; + /// Curry this function, transforming it from + /// + /// `fn(A, B) -> R` + /// to + /// `fn(A) -> fn(B) -> R` + fn curry(self, a: A) -> Self::Output; +} + +impl Curry for Func +where + Func: Fn(A, B) -> Out + 'static, + A: Copy + 'static, +{ + type Output = Box Out>; + + fn curry(self, a: A) -> Self::Output { + Box::new(move |b: B| self(a, b)) + } +} + +// not working magic !!! ))) +// impl FnOnce<(A,)> for Func +// where +// Func: Curry, +// { +// type Output = >::Output; +// extern "rust-call" fn call_once(self, args: (A,)) -> Self::Output { +// self.curry(args.0) +// } +// } + +/// Currying for functions with 3 params. +/// +/// See [Curry] for details. + +pub trait Curry2 { + /// The concrete type that `curry` returns. + type Output; + /// Curry this function, transforming it from + /// + /// `fn(A, B, C) -> R` + /// to + /// `fn(A) -> fn(B, C) -> R` + fn curry(self, a: A) -> Self::Output; +} + +impl Curry2 for Func +where + Func: Fn(A, B, C) -> Out + 'static, + A: Copy + 'static, +{ + type Output = Box Out>; + + fn curry(self, a: A) -> Self::Output { + Box::new(move |b: B, c: C| self(a, b, c)) + } +} + +/// Currying for functions with 4 params +/// +/// See [Curry] for details. +pub trait Curry3 { + /// The concrete type that `curry` returns. + type Output; + /// Curry this function, transforming it from + /// + /// `fn(A, B, C, D) -> R` + /// to + /// `fn(A) -> fn(B, C, D) -> R` + fn curry(self, a: A) -> Self::Output; +} + +impl Curry3 for Func +where + Func: Fn(A, B, C, D) -> Out + 'static, + A: Copy + 'static, +{ + type Output = Box Out>; + + fn curry(self, a: A) -> Self::Output { + Box::new(move |b: B, c: C, d: D| self(a, b, c, d)) + } +} + +/// Currying for functions with 5 params +/// +/// See [Curry] for details. +pub trait Curry4 { + /// The concrete type that `curry` returns. + type Output; + /// Curry this function, transforming it from + /// + /// `fn(A, B, C, D, E) -> R` + /// to + /// `fn(A) -> fn(B, C, D, E) -> R` + fn curry(self, a: A) -> Self::Output; +} + +impl Curry4 for Func +where + Func: Fn(A, B, C, D, E) -> Out + 'static, + A: Copy + 'static, +{ + type Output = Box Out>; + + fn curry(self, a: A) -> Self::Output { + Box::new(move |b: B, c: C, d: D, e: E| self(a, b, c, d, e)) + } +} diff --git a/src/prelude/mod.rs b/src/prelude/mod.rs index d77d70d..f4d3713 100644 --- a/src/prelude/mod.rs +++ b/src/prelude/mod.rs @@ -14,9 +14,15 @@ pub use self::builder::*; mod command; pub use self::command::*; +mod compose; +pub use self::compose::*; + mod controller; pub use self::controller::*; +mod currying; +pub use self::currying::*; + mod facade; pub use self::facade::*; @@ -32,6 +38,9 @@ pub use self::notification::*; mod notifier; pub use self::notifier::*; +// mod pipe; +// pub use self::pipe::*; + mod observer; pub use self::observer::*; @@ -43,3 +52,20 @@ pub use self::singleton::*; mod view; pub use self::view::*; + +pub use crate::utils::*; + +// pub use ruex_macro::aspect; +// pub use ruex_macro::decorate; +// pub use ruex_macro::delegate; +// pub use ruex_macro::register; +// pub use ruex_macro::Compose; +pub use ruex_macro::*; + +/// Joint point stub for aspect development +pub struct AspectJointPoint; + +impl AspectJointPoint { + /// Marker for joint point + pub fn proceed() {} +} diff --git a/src/prelude/pipe.rs b/src/prelude/pipe.rs new file mode 100644 index 0000000..ccca345 --- /dev/null +++ b/src/prelude/pipe.rs @@ -0,0 +1,470 @@ +/// Helper trait to call free functions using method call syntax. +/// +/// Rust already allows calling methods using function call syntax, but not the other way around. +/// This crate fills the gap by providing a simple helper trait `Pipe`. +/// +/// Inspired by https://github.com/xzfc/ufcs.rs, https://github.com/ear7h/plumb +/// and borrowed from https://github.com/KSXGitHub/pipe-trait.git +/// +/// ## See also +/// +/// Roughtly the same feature is either implemented or proposed in various languages. +/// +/// ### Rust +/// +/// * [`pipeline.rs`](https://github.com/johannhof/pipeline.rs): implemented as an macro +/// * [RFC #289](https://github.com/rust-lang/rfcs/issues/289): Unified function / method call syntax +/// * [RFC #2049](https://github.com/rust-lang/rfcs/issues/2049): Piping data to functions +/// * [Method-cascading and pipe-forward operators proposal](https://internals.rust-lang.org/t/method-cascading-and-pipe-forward-operators-proposal/7384/59) +/// * [Rust #44874](https://github.com/rust-lang/rust/issues/44874): Tracking issue for `arbitrary_self_types` +/// +/// ### Other languages +/// +/// * https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax +/// * [Nim], [DLang]: built-in UFCS +/// * F#, [Elixir]: the pipe operator `|>` +/// * C++: [proposed](https://brevzin.github.io/c++/2019/04/13/ufcs-history/) +/// * [Nim]: https://nim-lang.org/docs/manual.html#procedures-method-call-syntax +/// * [DLang]: https://tour.dlang.org/tour/en/gems/uniform-function-call-syntax-ufcs +/// * [Elixir]: https://elixirschool.com/en/lessons/basics/pipe-operator/ +/// +/// Make it possible to chain regular functions. +/// +/// **API Overview:** +/// +/// By adding `use ruex::prelude::*`, 9 methods are added to all types: +/// +/// | identifier | pipe syntax | traditional syntax | +/// |:-----------------------:|:----------------------:|:-------------------:| +/// | `Pipe::pipe` | `x.pipe(f)` | `f(x)` | +/// | `Pipe::pipe_ref` | `x.pipe_ref(f)` | `f(&x)` | +/// | `Pipe::pipe_mut` | `x.pipe_mut(f)` | `f(&mut x)` | +/// | `Pipe::pipe_as_ref` | `x.pipe_as_ref(f)` | `f(x.as_ref())` | +/// | `Pipe::pipe_as_mut` | `x.pipe_as_mut(f)` | `f(x.as_mut())` | +/// | `Pipe::pipe_deref` | `x.pipe_deref(f)` | `f(&x)` | +/// | `Pipe::pipe_deref_mut` | `x.pipe_deref_mut(f)` | `f(&mut x)` | +/// | `Pipe::pipe_borrow` | `x.pipe_borrow(f)` | `f(x.borrow())` | +/// | `Pipe::pipe_borrow_mut` | `x.pipe_borrow_mut(f)` | `f(x.borrow_mut())` | +/// +/// **Example:** Same type +/// +/// ```rust +/// use ruex::prelude::*; +/// let inc = |x| x + 1; +/// let double = |x| x + x; +/// let square = |x| x * x; +/// let a = (123i32).pipe(inc).pipe(double).pipe(square); +/// let b = square(double(inc(123i32))); +/// assert_eq!(a, b); +/// ``` +/// +/// **Example:** Type transformation +/// +/// ```rust +/// use ruex::prelude::*; +/// let x = 'x'; +/// let a = x +/// .pipe(|x| (x, x, x)) // (char, char, char) +/// .pipe(|x| [x, x]) // [(char, char, char); 2] +/// .pipe(|x| format!("{:?}", x)); // String +/// let b = "[('x', 'x', 'x'), ('x', 'x', 'x')]"; +/// assert_eq!(a, b); +/// ``` +/// +/// **Example:** Pipe amongst method chain +/// +/// ```rust +/// # async { +/// # use std::fmt::*; +/// # use futures::future::*; +/// # #[derive(Debug, Copy, Clone)] +/// # struct Num(pub i32); +/// # impl Num { +/// # pub fn inc(&self) -> Self { Self(self.0 + 1) } +/// # pub fn double(&self) -> Self { Self(self.0 * 2) } +/// # pub fn square(&self) -> Self { Self(self.0 * self.0) } +/// # pub fn get(&self) -> i32 { self.0 } +/// # pub fn future(self) -> Ready { ready(self) } +/// # } +/// # let my_future = Num(12).future(); +/// use ruex::prelude::*; +/// fn log(x: X) -> X { +/// println!("value: {:?}", x); +/// x +/// } +/// my_future +/// .pipe(log) +/// .await +/// .pipe(log) +/// .inc() +/// .pipe(log) +/// .double() +/// .pipe(log) +/// .square() +/// .pipe(log) +/// .get() +/// .pipe(log); +/// # }; +/// ``` +/// +/// **Example:** Explicit type annotation +/// +/// ```rust +/// use ruex::prelude::*; +/// let x = "abc".to_string(); +/// let a = x +/// .pipe_ref::<&str, _>(AsRef::as_ref) +/// .chars() +/// .pipe::, _>(Box::new) +/// .collect::>(); +/// let b = vec!['a', 'b', 'c']; +/// assert_eq!(a, b); +/// ``` +// #![no_std] +use std::{ + borrow::{Borrow, BorrowMut}, + ops::{Deref, DerefMut}, +}; + +/// All sized types implement this trait. +pub trait Pipe { + /// Apply `f` to `self`. + /// + /// ``` + /// # #[derive(Debug, PartialEq, Eq)] + /// # struct Foo(i32); + /// # fn double(x: i32) -> i32 { x * 2 } + /// # use ruex::prelude::*; + /// assert_eq!( + /// 12.pipe(double).pipe(Foo), + /// Foo(double(12)), + /// ) + /// ``` + #[inline] + fn pipe(self, f: F) -> R + where + Self: Sized, + F: FnOnce(Self) -> R, + { + f(self) + } + + /// Apply `f` to `&self`. + /// + /// ``` + /// # use ruex::prelude::*; + /// #[derive(Debug, PartialEq, Eq)] + /// struct Foo(i32); + /// let a = Foo(12); + /// let b = a + /// .pipe_ref(|a| a.0) // a is not moved + /// .pipe(Foo); + /// assert_eq!(a, b); // a is used again + /// ``` + #[inline] + fn pipe_ref<'a, R, F>(&'a self, f: F) -> R + where + F: FnOnce(&'a Self) -> R, + { + f(self) + } + + /// Apply `f` to `&mut self`. + /// + /// ``` + /// # use ruex::prelude::*; + /// #[derive(Debug, PartialEq, Eq)] + /// struct Foo(i32, i32); + /// let mut a = Foo(0, 0); + /// a.pipe_mut(|a| a.0 = 12); + /// a.pipe_mut(|a| a.1 = 34); + /// assert_eq!(a, Foo(12, 34)); + /// ``` + #[inline] + fn pipe_mut<'a, R, F>(&'a mut self, f: F) -> R + where + F: FnOnce(&'a mut Self) -> R, + { + f(self) + } + + /// Apply `f` to `&self` where `f` takes a single parameter of type `Param` + /// and `Self` implements trait [`AsRef`]. + /// + /// ``` + /// # use ruex::prelude::*; + /// fn uppercase(x: &str) -> String { + /// x.to_uppercase() + /// } + /// let x: String = "abc".to_string(); + /// let y: String = x.pipe_as_ref(uppercase); + /// assert_eq!(y, "ABC"); + /// ``` + #[inline] + fn pipe_as_ref<'a, P, R, F>(&'a self, f: F) -> R + where + Self: AsRef

, + P: ?Sized + 'a, + F: FnOnce(&'a P) -> R, + { + f(self.as_ref()) + } + + /// Apply `f` to `&mut self` where `f` takes a single parameter of type `Param` + /// and `Self` implements trait [`AsMut`]. + /// + /// ``` + /// # use ruex::prelude::*; + /// fn modify(target: &mut [i32]) { + /// target[0] = 123; + /// } + /// let mut vec: Vec = vec![0, 1, 2, 3]; + /// vec.pipe_as_mut(modify); + /// assert_eq!(vec, vec![123, 1, 2, 3]); + /// ``` + #[inline] + fn pipe_as_mut<'a, P, R, F>(&'a mut self, f: F) -> R + where + Self: AsMut

, + P: ?Sized + 'a, + F: FnOnce(&'a mut P) -> R, + { + f(self.as_mut()) + } + + /// Apply `f` to `&self` where `f` takes a single parameter of type `Param` + /// and `Self` implements trait `Deref`. + /// + /// ``` + /// # use ruex::prelude::*; + /// fn uppercase(x: &str) -> String { + /// x.to_uppercase() + /// } + /// let x: String = "abc".to_string(); + /// let y: String = x.pipe_deref(uppercase); + /// assert_eq!(y, "ABC"); + /// ``` + #[inline] + fn pipe_deref<'a, Param, R, F>(&'a self, f: F) -> R + where + Self: Deref, + Param: ?Sized + 'a, + F: FnOnce(&'a Param) -> R, + { + f(self) + } + + /// Apply `f` to `&mut self` where `f` takes a single parameter of type `Param` + /// and `Self` implements trait [`DerefMut`]. + /// + /// ``` + /// # use ruex::prelude::*; + /// fn modify(target: &mut [i32]) { + /// target[0] = 123; + /// } + /// let mut vec: Vec = vec![0, 1, 2, 3]; + /// vec.pipe_deref_mut(modify); + /// assert_eq!(vec, vec![123, 1, 2, 3]); + /// ``` + #[inline] + fn pipe_deref_mut<'a, Param, R, F>(&'a mut self, f: F) -> R + where + Self: DerefMut, + Param: ?Sized + 'a, + F: FnOnce(&'a mut Param) -> R, + { + f(self) + } + + /// Apply `f` to `&self` where `f` takes a single parameter of type `Param` + /// and `Self` implements trait [`Borrow`]. + /// + /// ``` + /// # use ruex::prelude::*; + /// fn uppercase(x: &str) -> String { + /// x.to_uppercase() + /// } + /// let x: String = "abc".to_string(); + /// let y: String = x.pipe_borrow(uppercase); + /// assert_eq!(y, "ABC"); + /// ``` + #[inline] + fn pipe_borrow<'a, Param, R, F>(&'a self, f: F) -> R + where + Self: Borrow, + Param: ?Sized + 'a, + F: FnOnce(&'a Param) -> R, + { + f(self.borrow()) + } + + /// Apply `f` to `&mut self` where `f` takes a single parameter of type `Param` + /// and `Self` implements trait [`BorrowMut`]. + /// + /// ``` + /// # use ruex::prelude::*; + /// fn modify(target: &mut [i32]) { + /// target[0] = 123; + /// } + /// let mut vec: Vec = vec![0, 1, 2, 3]; + /// vec.pipe_borrow_mut(modify); + /// assert_eq!(vec, vec![123, 1, 2, 3]); + /// ``` + #[inline] + fn pipe_borrow_mut<'a, Param, R, F>(&'a mut self, f: F) -> R + where + Self: BorrowMut, + Param: ?Sized + 'a, + F: FnOnce(&'a mut Param) -> R, + { + f(self.borrow_mut()) + } +} +impl Pipe for T {} + +#[cfg(test)] +mod tests { + use futures::executor::block_on; + use futures::future::lazy; + + use super::Pipe; + + async fn async_fn(s: String) -> String { + lazy(|_| format!("a({})", s)).await + } + + fn result_fn(s: String) -> Result { + Ok(format!("r({})", s)) + } + + #[test] + fn simple() { + assert_eq!("foo".pipe(Some), Some("foo")); + } + + #[test] + fn chaining() { + let a: Result = block_on(async { + "foo" + .to_string() + .pipe(result_fn)? + .pipe(|x| format!("c({})", x)) + .pipe(async_fn) + .await + .replace("f", "b") + .pipe(Ok) + }); + + let b: Result = block_on(async { + Ok(async_fn(format!("c({})", result_fn("foo".to_string())?)) + .await + .replace("f", "b")) + }); + + assert_eq!(a, b); + assert_eq!(a, Ok(String::from("a(c(r(boo)))"))); + } + + #[test] + fn same_type() { + let x: i32 = 3; + let inc = |x| x + 1; + let double = |x| x + x; + let square = |x| x * x; + let a = (x).pipe(inc).pipe(double).pipe(square); + let b = square(double(inc(x))); + assert_eq!(a, b); + } + + #[test] + fn type_transformation() { + let x = 'x'; + let a = x.pipe(|x| (x, x, x)).pipe(|x| [x, x]); + let b = [('x', 'x', 'x'), ('x', 'x', 'x')]; + assert_eq!(a, b); + } + + #[test] + fn slice() { + let vec: &[i32] = &[0, 1, 2, 3]; + let vec = vec.pipe(|x: &[i32]| [x, &[4, 5, 6]].concat()); + assert_eq!(vec, [0, 1, 2, 3, 4, 5, 6]); + } + + #[test] + fn trait_object() { + use core::{cmp::PartialEq, fmt::Display, marker::Copy}; + fn run(x: impl AsRef + PartialEq + Display + Copy + ?Sized) { + let x = x.pipe(|x| x); + assert_eq!(x.as_ref(), "abc"); + } + run("abc"); + } + + #[test] + #[allow(clippy::blacklisted_name)] + fn pipe_ref() { + #[derive(Debug, PartialEq, Eq)] + struct FooBar(i32); + let foo = FooBar(12); + let bar = foo.pipe_ref(|x| x.0).pipe(FooBar); + assert_eq!(foo, bar); + } + + #[test] + #[allow(clippy::blacklisted_name)] + fn pipe_ref_lifetime_bound() { + #[derive(Debug, PartialEq, Eq)] + struct Foo; + fn f(foo: &'_ Foo) -> &'_ Foo { + foo + } + Foo.pipe_ref(f).pipe_ref(f); + } + + #[test] + #[allow(clippy::blacklisted_name)] + fn pipe_mut() { + #[derive(Debug, PartialEq, Eq)] + struct Foo(i32); + let mut foo = Foo(0); + foo.pipe_mut(|x| x.0 = 32); + assert_eq!(foo, Foo(32)); + } + + #[test] + #[allow(clippy::blacklisted_name)] + fn pipe_mut_lifetime_bound() { + #[derive(Debug, PartialEq, Eq)] + struct Foo(i32, i32, i32); + impl Foo { + pub fn new() -> Self { + Self(0, 0, 0) + } + pub fn set_0(&mut self, x: i32) -> &mut Self { + self.0 = x; + self + } + pub fn set_1(&mut self, x: i32) -> &mut Self { + self.1 = x; + self + } + pub fn set_2(&mut self, x: i32) -> &mut Self { + self.2 = x; + self + } + } + + let mut expected = Foo::new(); + let expected = expected.set_0(123).set_1(456).set_2(789); + + fn modify(foo: &mut Foo) -> &mut Foo { + foo.set_0(123).set_1(456).set_2(789); + foo + } + let mut actual = Foo::new(); + let actual = actual.pipe_mut(modify); + + assert_eq!(actual, expected); + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..619740f --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,28 @@ +//! The `ruex` utilites. +//! +//! ``` +//! # #![allow(unused_imports)] +//! use ruex::utils::*; +//! ``` + +pub mod terminal; + +pub mod panic; + +pub mod logging { + //! Configurable logging support. + //! + pub use fern::*; + + pub use syslog; + + pub use log::LevelFilter; +} + +pub use humantime::*; + +pub mod build { + //! Build script and macro support. + //! + pub use companion::*; +} diff --git a/src/utils/panic/handler.rs b/src/utils/panic/handler.rs new file mode 100644 index 0000000..67bc926 --- /dev/null +++ b/src/utils/panic/handler.rs @@ -0,0 +1,142 @@ +//! Here lives our custom panic handler and its support code +//! +//! ## Panic handler chaining +//! +//! Rust already has a pretty good panic handling mechanism that I see no need to reinvent. +//! As a way to keep this functionality while still being able to print out a "Report this" message, +//! we can simply chain the handlers together. +//! +//! The reason I am not making use of +//! [`std::panic::update_hook`](https://doc.rust-lang.org/std/panic/fn.update_hook.html) +//! is both because it is a nightly feature. Maybe this can be added in the future. + +use std::panic::PanicInfo; + +use url::Url; + +use crate::utils::terminal::{Colorize, Link, Stream}; + +use super::validate::validate_repository; + +/// Contains metadata that we pull from the client crate with a macro +#[derive(Debug)] +pub struct PackageMetadata<'a> { + /// The client package name + pub pkg_name: &'a str, + /// The client crate version (bin or example) + pub crate_name: &'a str, + /// The client crate version + pub version: &'a str, + /// This may or may not be a repository URL. + /// Requires validation through `validate_repository()` + pub repository: &'a str, +} + +/// Append a panic handler to the end of the chain of panic handlers +/// +/// This function will execute whatever the existing panic handler is *before* executing the new one. +/// Metadata is required to inform our crash reporter about the issue tracker URL. +pub fn handler(hook_fn: F, metadata: PackageMetadata<'static>) +where + F: Fn(&PanicInfo<'_>, &PackageMetadata) + Sync + Send + 'static, +{ + // Get the current panic handler + let prev_hook = std::panic::take_hook(); + + // Create a new panic handler that chains our custom panic handler with the previous one + std::panic::set_hook(Box::new(move |panic_info| { + // Call the previous panic handler + prev_hook(panic_info); + + // Call our custom panic handler + hook_fn(panic_info, &metadata); + })); +} + +/// A panic handler that prints out a "Report this" message. +/// +/// This will try to determine an issue tracker URL from the crate metadata and link it in the terminal with a pre-made message. +pub fn suggest_issue_tracker(info: &PanicInfo<'_>, metadata: &PackageMetadata) { + // If color is disabled, we need to inform the `colored` crate before we start printing + if cfg!(not(feature = "color")) { + colored::control::set_override(false); + } + + // We have access to the repo metadata. + // If we have not found a supported repository, we cannot proceed with linking an issue tracker. + // This requires validating the repository URL. + let mut report_url = None; + + if !&metadata.repository.is_empty() { + if let Some(provider) = validate_repository(&Url::parse(metadata.repository).unwrap()) { + report_url = Some(provider.build_issue_url(info, metadata)); + } + } + + // Print the message + println!( + "{}", + format!( + "\n---------------------- {} ----------------------\n", + "Crash Detected".bold() + ) + .red() + ); + + if let Some(url) = report_url { + // Print a message with a link to the issue tracker + println!( + "{}", + "This application has issue tracker support enabled.".italic() + ); + println!( + "{}", + "Click the link below to report this crash to the developers.".bold() + ); + + // If this terminal supports clickable links, use one of those + // NOTE: VSCode both reports having clickable link support *and* does not let you click on the link + if crate::utils::terminal::on(Stream::Stdout) + && !std::env::var("TERM_PROGRAM") + .unwrap_or("unknown".to_string()) + .contains("vscode") + { + let link = Link::new("Report Crash", url.as_str()); + println!("\n[{}]", link.to_string().cyan().bold()); + } else { + // Otherwise, just print the URL + println!("\n{}", url.to_string().cyan().bold()); + } + } else { + // Well, this is awkward. Someone is using this crate without adding a repository key to their Cargo.toml. + if cfg!(debug_assertions) { + // If we are in a debug build. Warn the developer. + println!( + "{}", + "This application has issue tracker support enabled.".italic() + ); + println!("However, it was not possible to determine the repository URL."); + println!( + "Please add a `{}` key to your {}.", + "repository".bright_green().bold(), + "Cargo.toml".cyan().bold() + ); + println!( + "{}", + "\nThere is also a chance your repository service is not supported\nYou can request support at: https://github.com/ewpratten/crashreport-rs".italic() + ); + } else { + // Just tell the user something went wrong + println!( + "{}", + "This application has issue tracker support enabled.".italic() + ); + println!("However, it was not possible to determine the issue tracker URL."); + } + } + + println!( + "{}", + "\n------------------------------------------------------------\n".red() + ) +} diff --git a/src/utils/panic/mod.rs b/src/utils/panic/mod.rs new file mode 100644 index 0000000..f08af1a --- /dev/null +++ b/src/utils/panic/mod.rs @@ -0,0 +1,17 @@ +//! The panic handler better behaviour support. +//! + +mod handler; +pub use self::handler::*; + +mod provider; +pub use self::provider::*; + +mod validate; +pub use self::validate::*; + +pub mod backtrace { + //! The color backtrace support. + //! + pub use color_backtrace::*; +} diff --git a/src/utils/panic/provider.rs b/src/utils/panic/provider.rs new file mode 100644 index 0000000..ebff6ab --- /dev/null +++ b/src/utils/panic/provider.rs @@ -0,0 +1,63 @@ +use std::{panic::PanicInfo, time::SystemTime}; + +use url::Url; + +use super::handler::PackageMetadata; + +/// Describes supported repository providers. +#[derive(Debug)] +pub enum RepositoryProvider { + /// Github provider + GitHub(Url), + /// Gitlab provider + GitLab(Url), +} + +impl RepositoryProvider { + /// Returns the full URL required to file an issue in the repository of the client crate. + /// + /// Most Git providers allow issue creation through query params + pub fn build_issue_url(&self, info: &PanicInfo<'_>, metadata: &PackageMetadata) -> Url { + // No matter the provider, some of the strings are the same. + let body = vec![ + "\n

Information

\n\n", + "\n

Error

\n\n```", + &info.to_string(), + "```", + "\n

Additional Info

\n\n```", + &format!("OS: {}", std::env::consts::OS), + &format!("Architecture: {}", std::env::consts::ARCH), + &format!("Timestamp: {:?}", SystemTime::now()), + &format!("Version: {}", metadata.version), + &format!("Package: {}", metadata.pkg_name), + &format!("Crate: {}", metadata.crate_name), + "```\n", + "---", + "*This issue was auto-generated by the [`crashreport`](https://github.com/ewpratten/crashreport-rs) crate.*", + "*If you would like to improve these diagnostics, please contribute on GitHub.*" + ].join("\n"); + + match self { + RepositoryProvider::GitHub(url) => { + let mut output_url = url.clone(); + let path = url.path(); + output_url.set_path(&format!("{}/issues/new", path)); + output_url.set_query(Some(&format!( + "body={}&labels=bug", + urlencoding::encode(&body) + ))); + output_url + } + RepositoryProvider::GitLab(url) => { + let mut output_url = url.clone(); + let path = url.path(); + output_url.set_path(&format!("{}/issues/new", path)); + output_url.set_query(Some(&format!( + "issue[description]={}&issuable_template=bug", + urlencoding::encode(&body) + ))); + output_url + } + } + } +} diff --git a/src/utils/panic/validate.rs b/src/utils/panic/validate.rs new file mode 100644 index 0000000..621523c --- /dev/null +++ b/src/utils/panic/validate.rs @@ -0,0 +1,20 @@ +use url::Url; + +use super::provider::RepositoryProvider; + +/// Given a URL, tries to figure out the service provider of the repository. +pub fn validate_repository(url: &Url) -> Option { + // Handle assumptions about the provider. + // NOTE: only one assume_* feature can be enabled at a time. Enabling more than one will cause the first to be used. + if cfg!(feature = "assume_github") { + return Some(RepositoryProvider::GitHub(url.clone())); + } else if cfg!(feature = "assume_gitlab") { + return Some(RepositoryProvider::GitLab(url.clone())); + } + + match url.host_str() { + Some("github.com") => Some(RepositoryProvider::GitHub(url.clone())), + Some("gitlab.com") => Some(RepositoryProvider::GitLab(url.clone())), + _ => None, + } +} diff --git a/src/utils/terminal/mod.rs b/src/utils/terminal/mod.rs new file mode 100644 index 0000000..eafe833 --- /dev/null +++ b/src/utils/terminal/mod.rs @@ -0,0 +1,100 @@ +//! The terminal color and links support. +//! +//! +//! ``` +//! # #![allow(unused_imports)] +//! use ruex::utils::terminal::*; +//! ``` + +use std::fmt; + +pub use colored::*; + +pub use atty::Stream; + +/// Returns true if the current terminal, detected through various environment +/// variables, is known to support hyperlink rendering. +pub fn supports_hyperlinks() -> bool { + // Hyperlinks can be forced through this env var. + if let Ok(arg) = std::env::var("FORCE_HYPERLINK") { + return arg.trim() != "0"; + } + + if std::env::var("DOMTERM").is_ok() { + // DomTerm + return true; + } + + if let Ok(version) = std::env::var("VTE_VERSION") { + // VTE-based terminals above v0.50 (Gnome Terminal, Guake, ROXTerm, etc) + if version.parse().unwrap_or(0) >= 5000 { + return true; + } + } + + if let Ok(program) = std::env::var("TERM_PROGRAM") { + if matches!( + &program[..], + "Hyper" | "iTerm.app" | "terminology" | "WezTerm" + ) { + return true; + } + } + + if let Ok(term) = std::env::var("TERM") { + // Kitty + if matches!(&term[..], "xterm-kitty") { + return true; + } + } + + // Windows Terminal and Konsole + std::env::var("WT_SESSION").is_ok() || std::env::var("KONSOLE_VERSION").is_ok() +} + +/// Returns true if `stream` is a TTY, and the current terminal +/// [supports_hyperlinks]. +pub fn on(stream: atty::Stream) -> bool { + (std::env::var("FORCE_HYPERLINK").is_ok() || atty::is(stream)) && supports_hyperlinks() +} + +/// A clickable link component. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct Link<'a> { + /// link identifier + pub id: &'a str, + /// link text + pub text: &'a str, + /// link url + pub url: &'a str, +} + +impl<'a> Link<'a> { + /// Create a new link with a name and target url. + pub fn new(text: &'a str, url: &'a str) -> Self { + Self { text, url, id: "" } + } + + /// Create a new link with a name, a target url and an id. + pub fn with_id(text: &'a str, url: &'a str, id: &'a str) -> Self { + Self { text, url, id } + } +} + +impl fmt::Display for Link<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !self.id.is_empty() { + write!( + f, + "\u{1b}]8;id={};{}\u{1b}\\{}\u{1b}]8;;\u{1b}\\", + self.id, self.url, self.text + ) + } else { + write!( + f, + "\u{1b}]8;;{}\u{1b}\\{}\u{1b}]8;;\u{1b}\\", + self.url, self.text + ) + } + } +}