diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d2f4a74 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +indent_style = tab +end_of_line = lf +insert_final_newline = true +indent_size = 4 + +[*.md] +indent_style = space + +[*.yml] +indent_style = space + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..04062d0 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: +- package-ecosystem: cargo + directory: "/" + schedule: + interval: daily + time: "08:30" + open-pull-requests-limit: 10 diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 0000000..3367aa7 --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,46 @@ +name: Pull Request + +on: [pull_request] + +jobs: + test: + strategy: + matrix: + version: ['stable', 'nightly'] + runs-on: [ubuntu-latest] + timeout-minutes: 5 + steps: + - uses: actions/checkout@v2 + - name: Install latest Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + - name: Test + run: cargo test + - name: Build + run: cargo build --release + + lint: + runs-on: [ubuntu-latest] + timeout-minutes: 5 + steps: + - uses: actions/checkout@v2 + - name: Install latest Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + - name: Install latest nightly Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + profile: minimal + - name: Lint + run: "./scripts/lint.bash" + - name: Docs + run: "./scripts/docs.bash" + - name: Format + run: "./scripts/format.bash" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..299be50 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +coverage +target diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..a8961e6 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,61 @@ +binop_separator = "Front" +blank_lines_lower_bound = 0 +blank_lines_upper_bound = 1 +brace_style = "SameLineWhere" +color = "Auto" +combine_control_expr = false +comment_width = 120 +condense_wildcard_suffixes = true +control_brace_style = "ClosingNextLine" +disable_all_formatting = false +empty_item_single_line = true +enum_discrim_align_threshold = 0 +error_on_line_overflow = true +error_on_unformatted = true +fn_args_layout = "Tall" +fn_single_line = false +force_explicit_abi = true +force_multiline_blocks = true +format_code_in_doc_comments = true +format_macro_bodies = true +format_macro_matchers = false +format_strings = true +hard_tabs = true +hide_parse_errors = false +imports_indent = "Block" +imports_layout = "HorizontalVertical" +imports_granularity = "Crate" +indent_style = "block" +inline_attribute_width = 0 +match_arm_blocks = true +match_arm_leading_pipes = "Never" +match_block_trailing_comma = true +max_width = 120 +merge_derives = true +newline_style = "Unix" +normalize_comments = false +normalize_doc_attributes = true +overflow_delimited_expr = true +remove_nested_parens = true +reorder_impl_items = true +reorder_imports = true +group_imports = "StdExternalCrate" +reorder_modules = true +report_fixme = "Never" +report_todo = "Never" +skip_children = false +space_after_colon = true +space_before_colon = false +spaces_around_ranges = false +struct_field_align_threshold = 0 +struct_lit_single_line = true +tab_spaces = 4 +trailing_comma = "Vertical" +trailing_semicolon = true +type_punctuation_density = "Wide" +unstable_features = true +use_field_init_shorthand = true +use_small_heuristics = "Default" +use_try_shorthand = true +where_single_line = true +wrap_comments = true diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..feb815f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Change Log +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). + +## 0.1.0 - 2021-07-01 + +### Added +- Initial project release diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f41847b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at me@mitmaro.ca. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..30b267d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "captur" +version = "0.1.0" +authors = ["Tim Oram "] +description = "Macro to capture whole structs from disjoint fields in a closure." +repository = "https://github.com/MitMaro/captur" +documentation = "https://docs.rs/captur/" +keywords = [ "macro", "capture", "struct" ] +categories = ["rust-patterns"] +readme = "README.md" +license = "ISC" +include = [ + "**/*.rs", + "/Cargo.toml", + "/CHANGELOG.md", + "/LICENSE", + "/README.md" +] +edition = "2018" + +[lib] +name = "captur" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5489caf --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +Copyright (c) 2021, Tim Oram and all contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice appear +in all copies. + +The software is provided "as is" and the author disclaims all +warranties with regard to this software including all implied +warranties of merchantability and fitness. In no event shall the +author be liable for any special, direct, indirect, or +consequential damages or any damages whatsoever resulting from +loss of use, data or profits, whether in an action of contract, +negligence or other tortious action, arising out of or in connection +with the use or performance of this software. diff --git a/README.md b/README.md new file mode 100644 index 0000000..85ccd21 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +[![Crates.io](https://img.shields.io/crates/v/captur.svg)](https://crates.io/crates/captur) +[![docs](https://docs.rs/captur/badge.svg)](https://docs.rs/captur/) +[![GitHub license](https://img.shields.io/github/license/MitMaro/http-request-header)](https://raw.githubusercontent.com/MitMaro/captur/master/LICENSE) + +# Captur + +Starting in Rust 2021, Rust will no longer capture whole structs and instead will only capture a disjoint set of the fields used in a closure. In some cases, it is necessary to capture the structs to retain a particular drop order. This macro will capture the struct within the closure, ensuring the correct drop order. + +## The Fix + +The typical fix to this problem is to create an unused reference to the struct. + +```rust +let some_struct = SomeStruct::new(); +let result = || { + // capture some_struct within the closure + let _ = &some_struct; + println!("{}", some_struct.y); +} +``` + +While this is trivial to implement in closures where capturing is required, without a comment it, the meaning of the unused line is difficult to determine. This macro provides a self documenting and potentially more concise way to capture the structs. + +## Installation and Usage + +```toml +[dependencies] +captur = "0.1" +``` + +```rust +use captur::capture; + +fn send_event_and_action(action: &Action, event: Event) { + send(|sender| { + capture!(action, event); + sender.send(action.name.as_str(), event.code); + }); +} +``` + +## License + +Captur is released under the ISC license. See [LICENSE](LICENSE). diff --git a/scripts/docs.bash b/scripts/docs.bash new file mode 100755 index 0000000..6946403 --- /dev/null +++ b/scripts/docs.bash @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -e +set -u +set -o pipefail + +rustup update stable +cargo +stable doc --all-features + +# try to run the nightly version, if it exists, but do not fail +( + rustup update nightly && \ + cargo +nightly doc --all-features +) || true diff --git a/scripts/format.bash b/scripts/format.bash new file mode 100755 index 0000000..a454b4a --- /dev/null +++ b/scripts/format.bash @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -e +set -u +set -o pipefail + +RUST_VERSION="nightly" + +rustup update "$RUST_VERSION" +rustup component add rustfmt --toolchain "$RUST_VERSION" +cargo +"$RUST_VERSION" fmt --all -- --check + diff --git a/scripts/lint.bash b/scripts/lint.bash new file mode 100755 index 0000000..41fc8f1 --- /dev/null +++ b/scripts/lint.bash @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -e +set -u +set -o pipefail + +rustup update stable +rustup component add clippy --toolchain stable +cargo +stable clippy --all-targets --all-features + +# try to run the nightly version, if it exists, but do not fail +( + rustup update nightly && \ + rustup component add clippy --toolchain nightly && \ + cargo +nightly clippy --all-targets --all-features +) || true diff --git a/scripts/test.bash b/scripts/test.bash new file mode 100755 index 0000000..55c058d --- /dev/null +++ b/scripts/test.bash @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e +set -u +set -o pipefail + +rustup update stable +cargo test diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..eb2f7a6 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,142 @@ +#![deny( + future_incompatible, + nonstandard_style, + rust_2018_compatibility, + rust_2018_idioms, + unused, + warnings +)] +// rustc's additional allowed by default lints +#![deny( + absolute_paths_not_starting_with_crate, + deprecated_in_future, + elided_lifetimes_in_paths, + explicit_outlives_requirements, + keyword_idents, + macro_use_extern_crate, + meta_variable_misuse, + missing_abi, + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + non_ascii_idents, + noop_method_call, + or_patterns_back_compat, + pointer_structural_match, + semicolon_in_expressions_from_macros, + single_use_lifetimes, + trivial_casts, + trivial_numeric_casts, + unreachable_pub, + unsafe_code, + unsafe_op_in_unsafe_fn, + unstable_features, + unused_crate_dependencies, + unused_extern_crates, + unused_import_braces, + unused_lifetimes, + unused_qualifications, + unused_results, + variant_size_differences +)] +// enable all of Clippy's lints +#![deny(clippy::all, clippy::cargo, clippy::nursery, clippy::pedantic, clippy::restriction)] +#![allow( + clippy::blanket_clippy_restriction_lints, + clippy::implicit_return, + clippy::missing_docs_in_private_items, + clippy::redundant_pub_crate, + clippy::tabs_in_doc_comments +)] +#![deny( + rustdoc::bare_urls, + rustdoc::broken_intra_doc_links, + rustdoc::invalid_codeblock_attributes, + rustdoc::invalid_html_tags, + rustdoc::missing_crate_level_docs, + rustdoc::private_doc_tests, + rustdoc::private_intra_doc_links +)] + +//! # Captur +//! +//! Starting in Rust 2021, Rust will no longer capture whole structs and instead will only capture a +//! disjoint set of the fields used in a closure. In some cases, it is necessary to capture the +//! structs to retain a particular drop order. This macro will capture the struct within the +//! closure, ensuring the correct drop order. +//! +//! # Example +//! ``` +//! use captur::capture; +//! struct SomeStruct { +//! a: String, +//! b: String, +//! } +//! +//! impl SomeStruct { +//! fn new() -> Self { +//! Self { +//! a: String::from("a"), +//! b: String::from("b"), +//! } +//! } +//! } +//! +//! let some_struct = SomeStruct::new(); +//! let result = || { +//! captur::capture!(some_struct); +//! format!("{}", some_struct.b) +//! }; +//! +//! println!("{}", result()); +//! ``` + +/// Create a reference to a struct, that will ensure it is captured by a closure. +#[macro_export] +macro_rules! capture { + ($( $v:expr ),*) => { + $(let _ = &$v;)* + }; +} + +#[cfg(test)] +mod tests { + use super::*; + + struct TestStruct { + a: String, + b: String, + } + + impl TestStruct { + fn new() -> Self { + Self { + a: String::from("a"), + b: String::from("b"), + } + } + } + + #[test] + fn single() { + let a = TestStruct::new(); + let result = || { + capture!(a); + format!("Value: {}", a.a) + }; + + assert_eq!(result(), "Value: a"); + } + + #[test] + fn multiple() { + let a = TestStruct::new(); + let b = TestStruct::new(); + let result = || { + capture!(a, b); + format!("Value: {} {}", a.a, b.b) + }; + + assert_eq!(result(), "Value: a b"); + } +}