├── .github ├── .release-please-manifest.json ├── FUNDING.yml ├── release-please-config.json ├── release-please.yml ├── renovate.json └── workflows │ ├── ci.yaml │ └── publish.yaml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── deny.toml ├── rustfmt.toml ├── tests ├── e2e.rs └── unformat.rs ├── unfmt.rs └── unfmt_macros ├── CHANGELOG.md ├── Cargo.toml └── unfmt_macros.rs /.github/.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "0.2.2", 3 | "unfmt_macros": "0.2.2" 4 | } 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: mathematic-inc 4 | -------------------------------------------------------------------------------- /.github/release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", 3 | "group-pull-request-title-pattern": "chore: release${component} ${version}", 4 | "pull-request-title-pattern": "chore: release${component} ${version}", 5 | "pull-request-header": "Here is a summary of this release.", 6 | "last-release-sha": "75a4a32be933f0453be57eacdad95e1839e1b343", 7 | "release-type": "rust", 8 | "bump-minor-pre-major": true, 9 | "bump-patch-for-minor-pre-major": true, 10 | "packages": { 11 | ".": { 12 | "component": "unfmt", 13 | "include-component-in-tag": false 14 | }, 15 | "unfmt_macros": {} 16 | }, 17 | "plugins": [ 18 | "sentence-case", 19 | { 20 | "type": "cargo-workspace", 21 | "merge": false 22 | }, 23 | { 24 | "type": "linked-versions", 25 | "groupName": "unfmt", 26 | "components": ["unfmt", "unfmt_macros"] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.github/release-please.yml: -------------------------------------------------------------------------------- 1 | manifest: true 2 | handleGHRelease: true 3 | manifestConfig: .github/release-please-config.json 4 | manifestFile: .github/.release-please-manifest.json 5 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended"], 4 | "packageRules": [ 5 | { 6 | "matchManagers": ["github-actions", "cargo", "regex"], 7 | "groupName": "{{manager}}", 8 | "automerge": true 9 | } 10 | ], 11 | "customManagers": [ 12 | { 13 | "customType": "regex", 14 | "fileMatch": [ 15 | "(^|/)(workflow-templates|.(?:github|gitea|forgejo)/(?:workflows|actions))/.+.ya?ml$", 16 | "(^|/)action.ya?ml$" 17 | ], 18 | "matchStringsStrategy": "recursive", 19 | "matchStrings": [ 20 | "taiki-e/install-action(?:[\\s\\S]+?)tool:\\s*(.*)", 21 | "(?[^@\\s]*)@(?[^,\\s]*)" 22 | ], 23 | "versioningTemplate": "semver", 24 | "datasourceTemplate": "crate" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous integration 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | merge_group: 9 | branches: [main] 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | checks: 17 | name: Check code 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Check out repository 21 | uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 22 | - name: Set up Rust 23 | run: rustup default nightly 24 | - name: Install development tools 25 | uses: taiki-e/install-action@58a3efb22c892c0e7d722787ab604b97b62ac694 26 | with: 27 | tool: cargo-deny@0.16.1, cargo-udeps@0.1.50, cargo-hack@0.6.31 28 | - name: Install Rust linters 29 | run: rustup component add clippy rustfmt 30 | - name: Run checks 31 | run: | 32 | cargo fmt --check --all 33 | cargo hack --feature-powerset check --locked --workspace 34 | cargo hack --feature-powerset clippy --locked --workspace -- -D warnings 35 | cargo deny check 36 | cargo udeps --locked --workspace 37 | tests: 38 | name: Test on ${{ matrix.os.name }} (${{ matrix.channel }}) 39 | runs-on: ${{ matrix.os.value }} 40 | strategy: 41 | matrix: 42 | os: 43 | - name: Linux 44 | value: ubuntu-latest 45 | - name: Windows 46 | value: windows-latest 47 | - name: macOS 48 | value: macos-latest 49 | channel: 50 | - stable 51 | - beta 52 | - nightly 53 | steps: 54 | - name: Check out repository 55 | uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 56 | - name: Set up Rust 57 | run: rustup default ${{ matrix.channel }} 58 | - name: Install development tools 59 | uses: taiki-e/install-action@58a3efb22c892c0e7d722787ab604b97b62ac694 60 | with: 61 | tool: cargo-hack@0.6.31 62 | - name: Run tests 63 | run: cargo hack --feature-powerset test --locked 64 | coverage: 65 | name: Coverage 66 | runs-on: ubuntu-latest 67 | steps: 68 | - name: Check out repository 69 | uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 70 | - name: Set up Rust 71 | run: rustup default nightly 72 | - name: Install development tools 73 | uses: taiki-e/install-action@58a3efb22c892c0e7d722787ab604b97b62ac694 74 | with: 75 | tool: cargo-llvm-cov@0.6.13, cargo-hack@0.6.31 76 | - name: Create directories 77 | run: mkdir -p target/llvm-cov/lcov 78 | - name: Generate code coverage 79 | run: | 80 | cargo hack --feature-powerset llvm-cov --no-report --branch --locked --workspace 81 | cargo llvm-cov report --fail-under-lines 85 --lcov --output-path target/llvm-cov/lcov/${{ github.event.repository.name }}.info 82 | - name: Upload code coverage 83 | uses: romeovs/lcov-reporter-action@87a815f34ec27a5826abba44ce09bbc688da58fd 84 | if: github.event_name == 'pull_request' 85 | with: 86 | lcov-file: target/llvm-cov/lcov/${{ github.event.repository.name }}.info 87 | delete-old-comments: true 88 | success: 89 | name: Success 90 | needs: [checks, tests, coverage] 91 | if: always() 92 | runs-on: ubuntu-latest 93 | steps: 94 | - name: Check jobs 95 | uses: re-actors/alls-green@release/v1 96 | with: 97 | jobs: ${{ toJSON(needs) }} 98 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | publish: 10 | name: Publish 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out repository 14 | uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 15 | - name: Publish 16 | run: | 17 | cargo publish --locked --token ${{ secrets.CRATES_IO_TOKEN }} -p unfmt_macros 18 | cargo publish --locked --token ${{ secrets.CRATES_IO_TOKEN }} 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.2.2](https://github.com/mathematic-inc/unfmt/compare/v0.2.1...v0.2.2) (2024-05-14) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * **deps:** Update rust crate proc-macro2 to v1.0.82 ([#58](https://github.com/mathematic-inc/unfmt/issues/58)) ([e2bee12](https://github.com/mathematic-inc/unfmt/commit/e2bee129c9ff958fea796a23b8da2d48e09f6152)) 9 | * **deps:** Update rust crate syn to v2.0.61 ([#56](https://github.com/mathematic-inc/unfmt/issues/56)) ([d5fc873](https://github.com/mathematic-inc/unfmt/commit/d5fc8733f4b016df6352cc9c2f98cd6dd7604c33)) 10 | * **deps:** Update rust crate syn to v2.0.62 ([#65](https://github.com/mathematic-inc/unfmt/issues/65)) ([e7192e7](https://github.com/mathematic-inc/unfmt/commit/e7192e744089e6fb80ed6aeb72e9e6336f927a8a)) 11 | * **deps:** Update rust crate syn to v2.0.63 ([#69](https://github.com/mathematic-inc/unfmt/issues/69)) ([8b3f3cd](https://github.com/mathematic-inc/unfmt/commit/8b3f3cd3ad218b4934affb4413635ce407910cd7)) 12 | * Parse `syn::Lit` instead of `syn::Expr` ([#72](https://github.com/mathematic-inc/unfmt/issues/72)) ([c3621f8](https://github.com/mathematic-inc/unfmt/commit/c3621f83f383a97b66229595a31ef3ec13d8a1ba)) 13 | 14 | 15 | ### Dependencies 16 | 17 | * The following workspace dependencies were updated 18 | * dependencies 19 | * unfmt_macros bumped from 0.2.1 to 0.2.2 20 | 21 | ## [0.2.1](https://github.com/mathematic-inc/unfmt/compare/v0.2.0...v0.2.1) (2024-05-04) 22 | 23 | 24 | ### Bug Fixes 25 | 26 | * Fix escapes ([d17085e](https://github.com/mathematic-inc/unfmt/commit/d17085ef1dd1516c66b386b5d3bb265ae1e92407)) 27 | 28 | 29 | ### Dependencies 30 | 31 | * The following workspace dependencies were updated 32 | * dependencies 33 | * unfmt_macros bumped from 0.2.0 to 0.2.1 34 | 35 | ## [0.2.0](https://github.com/mathematic-inc/unfmt/compare/v0.1.3...v0.2.0) (2024-05-04) 36 | 37 | 38 | ### ⚠ BREAKING CHANGES 39 | 40 | * return non-tuple for single captures 41 | 42 | ### Features 43 | 44 | * Return non-tuple for single captures ([a068904](https://github.com/mathematic-inc/unfmt/commit/a0689041fd3ba0ef38380a47d1805077cd9a9d26)) 45 | 46 | 47 | ### Bug Fixes 48 | 49 | * **deps:** Update cargo ([#42](https://github.com/mathematic-inc/unfmt/issues/42)) ([87a4a6b](https://github.com/mathematic-inc/unfmt/commit/87a4a6b7ab377ce88e9cd1e5daf54a8d531c9b90)) 50 | 51 | 52 | ### Dependencies 53 | 54 | * The following workspace dependencies were updated 55 | * dependencies 56 | * unfmt_macros bumped from 0.1.3 to 0.2.0 57 | 58 | ## [0.1.3](https://github.com/mathematic-inc/unfmt/compare/v0.1.2...v0.1.3) (2024-04-25) 59 | 60 | 61 | ### Bug Fixes 62 | 63 | * **deps:** Update cargo ([#34](https://github.com/mathematic-inc/unfmt/issues/34)) ([b957d02](https://github.com/mathematic-inc/unfmt/commit/b957d021a0bbc43473e0d3818c54ecfa979c83a4)) 64 | 65 | 66 | ### Dependencies 67 | 68 | * The following workspace dependencies were updated 69 | * dependencies 70 | * unfmt_macros bumped from 0.1.2 to 0.1.3 71 | 72 | ## [0.1.2](https://github.com/mathematic-inc/unfmt/compare/v0.1.1...v0.1.2) (2024-04-15) 73 | 74 | 75 | ### Bug Fixes 76 | 77 | * Allow missing full match ([10fde84](https://github.com/mathematic-inc/unfmt/commit/10fde845468299ce6e01fb73592e9b60827920f0)), closes [#21](https://github.com/mathematic-inc/unfmt/issues/21) 78 | 79 | 80 | ### Dependencies 81 | 82 | * The following workspace dependencies were updated 83 | * dependencies 84 | * unfmt_macros bumped from 0.1.0 to 0.1.2 85 | 86 | ## [0.1.1](https://github.com/mathematic-inc/unfmt/compare/unfmt-v0.1.0...unfmt-v0.1.1) (2024-04-14) 87 | 88 | 89 | ### Bug Fixes 90 | 91 | * Split macro implementation into separate crate ([#14](https://github.com/mathematic-inc/unfmt/issues/14)) ([21a6897](https://github.com/mathematic-inc/unfmt/commit/21a6897714cf07a4496c7e291061ad2ff9dfd15b)) 92 | 93 | ## 0.1.0 (2024-02-11) 94 | 95 | 96 | ### Features 97 | 98 | * Initial commit ([35ae318](https://github.com/mathematic-inc/unfmt/commit/35ae318256722500ead9484e99df69641be840e1)) 99 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "bstr" 7 | version = "1.10.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" 10 | dependencies = [ 11 | "memchr", 12 | "regex-automata", 13 | "serde", 14 | ] 15 | 16 | [[package]] 17 | name = "memchr" 18 | version = "2.7.4" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 21 | 22 | [[package]] 23 | name = "proc-macro2" 24 | version = "1.0.86" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 27 | dependencies = [ 28 | "unicode-ident", 29 | ] 30 | 31 | [[package]] 32 | name = "quote" 33 | version = "1.0.37" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 36 | dependencies = [ 37 | "proc-macro2", 38 | ] 39 | 40 | [[package]] 41 | name = "regex-automata" 42 | version = "0.4.7" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 45 | 46 | [[package]] 47 | name = "serde" 48 | version = "1.0.210" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" 51 | dependencies = [ 52 | "serde_derive", 53 | ] 54 | 55 | [[package]] 56 | name = "serde_derive" 57 | version = "1.0.210" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" 60 | dependencies = [ 61 | "proc-macro2", 62 | "quote", 63 | "syn", 64 | ] 65 | 66 | [[package]] 67 | name = "syn" 68 | version = "2.0.77" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" 71 | dependencies = [ 72 | "proc-macro2", 73 | "quote", 74 | "unicode-ident", 75 | ] 76 | 77 | [[package]] 78 | name = "unfmt" 79 | version = "0.2.2" 80 | dependencies = [ 81 | "bstr", 82 | "unfmt_macros", 83 | ] 84 | 85 | [[package]] 86 | name = "unfmt_macros" 87 | version = "0.2.2" 88 | dependencies = [ 89 | "bstr", 90 | "proc-macro2", 91 | "quote", 92 | "syn", 93 | ] 94 | 95 | [[package]] 96 | name = "unicode-ident" 97 | version = "1.0.12" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 100 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "unfmt" 3 | version = "0.2.2" 4 | edition = "2021" 5 | description = "A compile-time pattern matching library that reverses the interpolation process of `format!`." 6 | license = "MIT OR Apache-2.0" 7 | authors = ["Mathematic Inc"] 8 | repository = "https://github.com/mathematic-inc/unfmt" 9 | categories = ["no-std", "value-formatting"] 10 | keywords = ["unformat", "regex"] 11 | 12 | [lib] 13 | path = "./unfmt.rs" 14 | 15 | [workspace] 16 | members = ["unfmt_macros"] 17 | 18 | [workspace.lints.clippy] 19 | all = "deny" 20 | pedantic = "deny" 21 | restriction = "deny" 22 | nursery = "deny" 23 | # REASON: We disable them when they are not idiomatic. 24 | blanket_clippy_restriction_lints = { level = "allow", priority = 1 } 25 | # REASON: Not idiomatic. 26 | implicit_return = { level = "allow", priority = 1 } 27 | # REASON: False-positives with macros. 28 | pub_use = { level = "allow", priority = 1 } 29 | # REASON: Incompatible with pattern_type_mismatch and other lints similar to it. 30 | ref_patterns = { level = "allow", priority = 1 } 31 | # REASON: Splitting is generally idiomatic. 32 | single_call_fn = { level = "allow", priority = 1 } 33 | # REASON: Some trait methods are meant to be not implemented. 34 | missing_trait_methods = { level = "allow", priority = 1 } 35 | # REASON: Not idiomatic. 36 | shadow_reuse = { level = "allow", priority = 1 } 37 | # REASON: Not idiomatic. 38 | shadow_same = { level = "allow", priority = 1 } 39 | # REASON: Sometimes code is really unreachable. 40 | unreachable = { level = "allow", priority = 1 } 41 | # REASON: Not idiomatic. 42 | question_mark_used = { level = "allow", priority = 1 } 43 | # REASON: Separated suffixes are more readable. 44 | separated_literal_suffix = { level = "allow", priority = 1 } 45 | # REASON: Too general. 46 | as_conversions = { level = "allow", priority = 1 } 47 | # REASON: Not idiomatic. 48 | self-named-module-files = { level = "allow", priority = 1 } 49 | # REASON: Too noisy. 50 | missing_docs_in_private_items = { level = "allow", priority = 1 } 51 | # REASON: Too noisy. 52 | exhaustive_structs = { level = "allow", priority = 1 } 53 | # REASON: Too noisy. 54 | exhaustive_enums = { level = "allow", priority = 1 } 55 | # REASON: Expect may be used for error handling. 56 | expect_used = { level = "allow", priority = 1 } 57 | # REASON: Too noisy. 58 | module_name_repetitions = { level = "allow", priority = 1 } 59 | # REASON: Clashes with clippy::pattern_type_mismatch 60 | needless_borrowed_reference = { level = "allow", priority = 1 } 61 | 62 | [dependencies] 63 | bstr = "1.9.1" 64 | unfmt_macros = { path = "unfmt_macros", version = "0.2.2" } 65 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Mathematic Inc 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # unfmt 2 | 3 | [![crates.io](https://img.shields.io/crates/v/unfmt?style=flat-square)](https://crates.io/crates/unfmt) 4 | [![license](https://img.shields.io/crates/l/unfmt?style=flat-square)](https://github.com/mathematic-inc/unfmt) 5 | [![ci](https://img.shields.io/github/actions/workflow/status/mathematic-inc/unfmt/ci.yaml?label=ci&style=flat-square)](https://github.com/mathematic-inc/unfmt/actions/workflows/ci.yaml) 6 | [![docs](https://img.shields.io/docsrs/unfmt?style=flat-square)](https://docs.rs/unfmt/latest/unfmt/) 7 | 8 | `unfmt` is a compile-time pattern matching library that reverses the 9 | interpolation process of `format!`. 10 | 11 | You can think of it as an extremely lightweight regular expression engine 12 | without the runtime pattern-compilation cost. 13 | 14 | ## Installation 15 | 16 | ```sh 17 | cargo add -D unfmt 18 | ``` 19 | 20 | ## Usage 21 | 22 | ```rs 23 | let value = "My name is Rho."; 24 | 25 | // Unnamed captures are returned as tuples. 26 | assert_eq!( 27 | unformat!("My {} is {}.", value), 28 | Some(("name", "Rho")) 29 | ); 30 | 31 | // You can put indices as well; just make sure ALL captures use indices 32 | // otherwise it's not well defined. 33 | assert_eq!( 34 | unformat!("My {1} is {0}.", value), 35 | Some(("Rho", "name")) 36 | ); 37 | 38 | // You can also name captures using variables, but make sure you check the 39 | // return is not None. 40 | let mut subject = None; 41 | let mut object = None; 42 | assert_eq!( 43 | unformat!("My {subject} is {object}.", value), 44 | Some(()) 45 | ); 46 | assert_eq!((subject, object), (Some("name"), Some("Rho"))); 47 | 48 | // If you want to ensure the entire string matches, you can add `true` to the end of the macro. 49 | assert_eq!(unformat!("{1} is {0}", value, true), None); 50 | 51 | // If a type implements `FromStr`, you can use it as a type argument. This 52 | // is written as `{:Type}`. 53 | assert_eq!( 54 | unformat!("Listening on {:url::Url}", "Listening on http://localhost:3000"), 55 | Some((url::Url::from_str("http://localhost:3000").unwrap(),)) 56 | ); 57 | ``` 58 | 59 | In general, captures are written as `{:}`. Multiple 60 | captures in a row (i.e. `{}{}`) are not supported as they aren't well-defined. 61 | 62 | ## Limitations 63 | 64 | - There is no backtracking. 65 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [licenses] 2 | unused-allowed-license = "allow" 3 | allow = [ 4 | "0BSD", 5 | "Apache-2.0", 6 | "BSD-2-Clause", 7 | "BSD-3-Clause", 8 | "BSD-4-Clause", 9 | "CC0-1.0", 10 | "ISC", 11 | "MIT-0", 12 | "MIT", 13 | "MS-PL", 14 | "Unicode-DFS-2016", 15 | "Unlicense", 16 | "Zlib", 17 | ] 18 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Crate" 2 | group_imports = "StdExternalCrate" -------------------------------------------------------------------------------- /tests/e2e.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env::{self, temp_dir}, 3 | fs::{create_dir_all, write}, 4 | }; 5 | 6 | #[test] 7 | fn test_e2e() { 8 | let e2e_dir = temp_dir().join("unfmt_e2e"); 9 | create_dir_all(e2e_dir.join("src")).expect("failed to create temp dir"); 10 | 11 | write( 12 | e2e_dir.join("src/main.rs"), 13 | r#"use unfmt::unformat;fn main() {unformat!("hello {}", "hello world");}"#, 14 | ) 15 | .expect("failed to write file"); 16 | 17 | let mut cargo_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"); 18 | if cfg!(windows) { 19 | cargo_dir = cargo_dir.replace("\\", "/"); 20 | } 21 | write( 22 | e2e_dir.join("Cargo.toml"), 23 | format! { r#" 24 | [package] 25 | name = "unfmt_e2e" 26 | version = "0.1.0" 27 | edition = "2021" 28 | 29 | [dependencies.unfmt] 30 | path = "{cargo_dir}" 31 | "#}, 32 | ) 33 | .expect("failed to write file"); 34 | 35 | let output = std::process::Command::new("cargo") 36 | .arg("run") 37 | .current_dir(&e2e_dir) 38 | .output() 39 | .expect("failed to run cargo"); 40 | 41 | if !output.status.success() { 42 | println!("tempdir: {}", e2e_dir.display()); 43 | println!( 44 | "stderr: {}", 45 | std::str::from_utf8(&output.stderr).expect("failed to convert stdout to string") 46 | ); 47 | println!( 48 | "stdout: {}", 49 | std::str::from_utf8(&output.stdout).expect("failed to convert stdout to string") 50 | ); 51 | } 52 | 53 | assert!(output.status.success()); 54 | } 55 | -------------------------------------------------------------------------------- /tests/unformat.rs: -------------------------------------------------------------------------------- 1 | #![cfg(test)] 2 | 3 | use std::{net::SocketAddr, str::FromStr}; 4 | 5 | use unfmt::unformat; 6 | 7 | #[test] 8 | fn test_unformat() { 9 | assert_eq!(unformat!("abc", "abc"), Some(())); 10 | assert_eq!(unformat!("abc", "abcd"), Some(())); 11 | assert_eq!(unformat!("abc", "acd"), None); 12 | } 13 | 14 | #[test] 15 | fn test_unformat_captures() { 16 | assert_eq!(unformat!("{}", "abc"), Some("abc")); 17 | assert_eq!(unformat!("{}bc", "abc"), Some("a")); 18 | assert_eq!(unformat!("a{}c", "abc"), Some("b")); 19 | assert_eq!(unformat!("ab{}", "abc"), Some("c")); 20 | assert_eq!(unformat!("{}{}c", "abc"), None); 21 | assert_eq!(unformat!("{}b{}", "abc"), Some(("a", "c"))); 22 | assert_eq!(unformat!("a{}c", "acd"), Some("")); 23 | } 24 | 25 | #[test] 26 | fn test_unformat_indexed_captures() { 27 | assert_eq!(unformat!("{1}b{0}", "abc"), Some(("c", "a"))); 28 | } 29 | 30 | #[test] 31 | fn test_unformat_named_captures() { 32 | let mut name = None; 33 | assert_eq!(unformat!("ab{name}", "abc"), Some(())); 34 | assert_eq!(name, Some("c")); 35 | } 36 | 37 | #[test] 38 | fn test_unformat_escaped_captures() { 39 | let mut name = None; 40 | assert_eq!(unformat!("a{{{name}}}c", "a{b}c"), Some(())); 41 | assert_eq!(name, Some("b")); 42 | } 43 | 44 | #[test] 45 | fn test_unformat_typed_captures() { 46 | assert_eq!(unformat!("ab{:usize}", "ab152"), Some(152)); 47 | assert_eq!( 48 | unformat!("ab{:SocketAddr}a", "ab127.0.0.1:3000a"), 49 | SocketAddr::from_str("127.0.0.1:3000").ok() 50 | ); 51 | } 52 | 53 | #[test] 54 | fn test_unformat_typed_named_captures() { 55 | let mut name = None; 56 | assert_eq!(unformat!("ab{name:usize}", "ab152"), Some(())); 57 | assert_eq!(name, Some(152)); 58 | 59 | let mut addr = None; 60 | assert_eq!( 61 | unformat!("ab{addr:SocketAddr}a", "ab127.0.0.1:3000a"), 62 | Some(()) 63 | ); 64 | assert_eq!(addr, SocketAddr::from_str("127.0.0.1:3000").ok()); 65 | } 66 | 67 | #[test] 68 | fn test_declmacro() { 69 | macro_rules! test_declmacro { 70 | ($fmt:literal, $input:expr) => { 71 | unformat!($fmt, $input) 72 | }; 73 | } 74 | 75 | test_declmacro!("abc", "abcd"); 76 | } 77 | 78 | #[test] 79 | fn test_full_match() { 80 | assert_eq!(unformat!("({:u8}, {:u8})", "(1, 2)"), Some((1, 2))); 81 | assert_eq!(unformat!("({:u8}, {:u8})", "(1, 2)bar"), Some((1, 2))); 82 | assert_eq!(unformat!("({:u8}, {:u8})", "foo(1, 2)"), Some((1, 2))); 83 | 84 | assert_eq!(unformat!("({:u8}, {:u8})", "(1, 2)", true), Some((1, 2))); 85 | assert_eq!(unformat!("({:u8}, {:u8})", "(1, 2)bar", true), None); 86 | assert_eq!(unformat!("({:u8}, {:u8})", "foo(1, 2)", true), None); 87 | } 88 | -------------------------------------------------------------------------------- /unfmt.rs: -------------------------------------------------------------------------------- 1 | pub use bstr; 2 | pub use unfmt_macros::*; 3 | -------------------------------------------------------------------------------- /unfmt_macros/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.2.2](https://github.com/mathematic-inc/unfmt/compare/unfmt_macros-v0.2.1...unfmt_macros-v0.2.2) (2024-05-14) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * Parse `syn::Lit` instead of `syn::Expr` ([#72](https://github.com/mathematic-inc/unfmt/issues/72)) ([c3621f8](https://github.com/mathematic-inc/unfmt/commit/c3621f83f383a97b66229595a31ef3ec13d8a1ba)) 9 | 10 | ## [0.2.1](https://github.com/mathematic-inc/unfmt/compare/unfmt_macros-v0.2.0...unfmt_macros-v0.2.1) (2024-05-04) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * Fix escapes ([d17085e](https://github.com/mathematic-inc/unfmt/commit/d17085ef1dd1516c66b386b5d3bb265ae1e92407)) 16 | 17 | ## [0.2.0](https://github.com/mathematic-inc/unfmt/compare/unfmt_macros-v0.1.3...unfmt_macros-v0.2.0) (2024-05-04) 18 | 19 | 20 | ### ⚠ BREAKING CHANGES 21 | 22 | * return non-tuple for single captures 23 | 24 | ### Features 25 | 26 | * Return non-tuple for single captures ([a068904](https://github.com/mathematic-inc/unfmt/commit/a0689041fd3ba0ef38380a47d1805077cd9a9d26)) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * **deps:** Update cargo ([#42](https://github.com/mathematic-inc/unfmt/issues/42)) ([87a4a6b](https://github.com/mathematic-inc/unfmt/commit/87a4a6b7ab377ce88e9cd1e5daf54a8d531c9b90)) 32 | 33 | ## [0.1.3](https://github.com/mathematic-inc/unfmt/compare/unfmt_macros-v0.1.2...unfmt_macros-v0.1.3) (2024-04-25) 34 | 35 | 36 | ### Bug Fixes 37 | 38 | * **deps:** Update cargo ([#34](https://github.com/mathematic-inc/unfmt/issues/34)) ([b957d02](https://github.com/mathematic-inc/unfmt/commit/b957d021a0bbc43473e0d3818c54ecfa979c83a4)) 39 | 40 | ## [0.1.2](https://github.com/mathematic-inc/unfmt/compare/unfmt_macros-v0.1.0...unfmt_macros-v0.1.2) (2024-04-15) 41 | 42 | 43 | ### Bug Fixes 44 | 45 | * Allow missing full match ([10fde84](https://github.com/mathematic-inc/unfmt/commit/10fde845468299ce6e01fb73592e9b60827920f0)), closes [#21](https://github.com/mathematic-inc/unfmt/issues/21) 46 | 47 | ## [0.1.0](https://github.com/mathematic-inc/unfmt/compare/unfmt_macros-v0.1.0...unfmt_macros-v0.1.0) (2024-04-14) 48 | 49 | 50 | ### Bug Fixes 51 | 52 | * Split macro implementation into separate crate ([#14](https://github.com/mathematic-inc/unfmt/issues/14)) ([21a6897](https://github.com/mathematic-inc/unfmt/commit/21a6897714cf07a4496c7e291061ad2ff9dfd15b)) 53 | -------------------------------------------------------------------------------- /unfmt_macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "unfmt_macros" 3 | version = "0.2.2" 4 | edition = "2021" 5 | description = "A compile-time pattern matching library that reverses the interpolation process of `format!`." 6 | license = "MIT OR Apache-2.0" 7 | authors = ["Mathematic Inc"] 8 | repository = "https://github.com/mathematic-inc/unfmt" 9 | categories = ["no-std", "value-formatting"] 10 | keywords = ["unformat", "regex"] 11 | 12 | [lib] 13 | proc-macro = true 14 | path = "./unfmt_macros.rs" 15 | 16 | [lints] 17 | workspace = true 18 | 19 | [dependencies] 20 | bstr = "1.9.1" 21 | syn = "2.0.60" 22 | quote = "1.0.36" 23 | proc-macro2 = "1.0.81" 24 | -------------------------------------------------------------------------------- /unfmt_macros/unfmt_macros.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | extern crate alloc; 4 | extern crate proc_macro; 5 | 6 | use alloc::{borrow::ToOwned, format, vec::Vec}; 7 | 8 | use bstr::ByteSlice; 9 | use proc_macro::TokenStream; 10 | use proc_macro2::Span; 11 | use quote::{quote, ToTokens}; 12 | use syn::{ 13 | parse::{Parse, ParseStream, Result}, 14 | parse_macro_input, parse_str, Expr, Ident, Lit, LitBool, LitByteStr, Token, TypePath, 15 | }; 16 | 17 | struct Unformat { 18 | pattern: Vec, 19 | text: Expr, 20 | is_pattern_str: bool, 21 | full_match: bool, 22 | } 23 | 24 | impl Parse for Unformat { 25 | fn parse(input: ParseStream) -> Result { 26 | #[expect( 27 | clippy::wildcard_enum_match_arm, 28 | reason = "We want to match on future variants as well." 29 | )] 30 | let (pattern, is_pattern_str) = match input.parse::()? { 31 | Lit::Str(str) => (str.value().into_bytes(), true), 32 | Lit::ByteStr(byte_str) => (byte_str.value(), false), 33 | _ => return Err(input.error("expected a string literal")), 34 | }; 35 | 36 | input.parse::()?; 37 | 38 | let text = input.parse::()?; 39 | 40 | let full_match = if input.parse::().is_ok() { 41 | input.parse::().map_or(false, |bool| bool.value) 42 | } else { 43 | false 44 | }; 45 | Ok(Self { 46 | pattern, 47 | text, 48 | is_pattern_str, 49 | full_match, 50 | }) 51 | } 52 | } 53 | 54 | enum Assignee { 55 | Index(u32), 56 | Variable(Ident), 57 | } 58 | 59 | impl Assignee { 60 | fn new(variable: &str, index: &mut u32) -> Self { 61 | variable.parse::().map_or_else( 62 | |_| { 63 | if variable.is_empty() { 64 | let tuple_index = *index; 65 | *index = index.saturating_add(1); 66 | Self::Index(tuple_index) 67 | } else { 68 | Self::Variable(parse_str(variable).expect("invalid variable name")) 69 | } 70 | }, 71 | Self::Index, 72 | ) 73 | } 74 | } 75 | 76 | enum CaptureTypePath { 77 | Str, 78 | Bytes, 79 | Typed(TypePath), 80 | } 81 | 82 | impl CaptureTypePath { 83 | fn new(type_path: &str, is_pattern_str: bool) -> Self { 84 | if type_path.is_empty() { 85 | if is_pattern_str { 86 | Self::Str 87 | } else { 88 | Self::Bytes 89 | } 90 | } else if type_path == "&str" { 91 | Self::Str 92 | } else if type_path == "&[u8]" { 93 | Self::Bytes 94 | } else { 95 | Self::Typed(parse_str(type_path).expect("invalid type path")) 96 | } 97 | } 98 | } 99 | 100 | impl ToTokens for CaptureTypePath { 101 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 102 | tokens.extend(match *self { 103 | Self::Str => { 104 | quote! { &str } 105 | } 106 | Self::Bytes => { 107 | quote! { &[u8] } 108 | } 109 | Self::Typed(ref type_path) => { 110 | quote! { #type_path } 111 | } 112 | }); 113 | } 114 | } 115 | 116 | struct Capture { 117 | text: Vec, 118 | assignee: Assignee, 119 | r#type: CaptureTypePath, 120 | } 121 | 122 | impl Capture { 123 | fn new(text: &[u8], capture: &str, is_pattern_str: bool, index: &mut u32) -> Self { 124 | let (variable, type_path) = capture.split_once(':').unwrap_or((capture, "")); 125 | Self { 126 | text: text.to_vec(), 127 | assignee: Assignee::new(variable, index), 128 | r#type: CaptureTypePath::new(type_path, is_pattern_str), 129 | } 130 | } 131 | } 132 | 133 | impl ToTokens for Capture { 134 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 135 | let rhs = match self.r#type { 136 | CaptureTypePath::Str => { 137 | quote! { 138 | if let Ok(__unfmt_left) = __unfmt_left.to_str() { 139 | __unfmt_left 140 | } else { 141 | break 'unformat None; 142 | } 143 | } 144 | } 145 | CaptureTypePath::Bytes => { 146 | quote! { __unfmt_left } 147 | } 148 | CaptureTypePath::Typed(ref type_path) => { 149 | quote! { 150 | if let Ok(Ok(__unfmt_left)) = __unfmt_left.to_str().map(|value| value.parse::<#type_path>()) { 151 | __unfmt_left 152 | } else { 153 | break 'unformat None; 154 | } 155 | } 156 | } 157 | }; 158 | let assignment = match self.assignee { 159 | Assignee::Index(ref index) => { 160 | let ident = Ident::new(&format!("__unfmt_capture_{index}"), Span::call_site()); 161 | quote! { let #ident = #rhs } 162 | } 163 | Assignee::Variable(ref ident) => { 164 | quote! { #ident = Some(#rhs) } 165 | } 166 | }; 167 | let text = LitByteStr::new(&self.text, Span::call_site()); 168 | 169 | // If text is empty, `find` will return `Some(0)` and the capture will 170 | // be at the end of the pattern, so this capture (`__unfmt_left`) would 171 | // be empty. Since captures are inherently .*? in regex, this capture 172 | // should consume the remainder of the text, so we swap `__unfmt_left` 173 | // and `__unfmt_right` to achieve this. 174 | tokens.extend(if self.text.is_empty() { 175 | quote! { let (__unfmt_left, __unfmt_right) = (__unfmt_byte_text, b""); } 176 | } else { 177 | quote! { 178 | let Some((__unfmt_left, __unfmt_right)) = __unfmt_byte_text.split_once_str(#text) else { 179 | break 'unformat None; 180 | }; 181 | } 182 | }); 183 | 184 | tokens.extend(quote! { 185 | #assignment; 186 | __unfmt_byte_text = BStr::new(__unfmt_right); 187 | }); 188 | } 189 | } 190 | 191 | /// Basic implementation of reversing the `format!` process. Matches a given 192 | /// text against a given pattern, returning any captures. 193 | /// 194 | /// Rules: 195 | /// 196 | /// - Patterns are substring matched. 197 | /// - Captures are written as `{?(:)?}` in the pattern. 198 | /// - Captures are similar to `(.*?)` in regex, but without backtracking. 199 | /// - Sequential captures (e.g. `{}{}`) are not supported and will return 200 | /// `None`. 201 | /// 202 | /// # Panics 203 | /// 204 | /// This function panics if the pattern is invalid. This includes: 205 | /// 206 | /// - Consecutive captures. 207 | /// - Unmatched `}` in the pattern. 208 | /// - Invalid UTF-8 in capture names. 209 | /// 210 | #[proc_macro] 211 | pub fn unformat(input: TokenStream) -> TokenStream { 212 | let Unformat { 213 | pattern, 214 | text, 215 | is_pattern_str, 216 | full_match, 217 | } = parse_macro_input!(input as Unformat); 218 | 219 | let (initial_part, captures) = compile(&pattern, is_pattern_str); 220 | let initial_part = Lit::ByteStr(LitByteStr::new(&initial_part, Span::call_site())); 221 | 222 | let capture_idents = { 223 | let mut capture_indices = captures 224 | .iter() 225 | .filter_map(|capture| match capture.assignee { 226 | Assignee::Index(capture_index) => Some(capture_index), 227 | Assignee::Variable(..) => None, 228 | }) 229 | .collect::>(); 230 | 231 | capture_indices.sort_by(|&index_a, &index_b| index_a.cmp(&index_b)); 232 | 233 | capture_indices 234 | .into_iter() 235 | .map(|index| Ident::new(&format!("__unfmt_capture_{index}"), Span::call_site())) 236 | .collect::>() 237 | }; 238 | 239 | let capture_block = if full_match { 240 | quote! { 241 | if !__unfmt_left.is_empty() { 242 | break 'unformat None; 243 | } 244 | #(#captures)* 245 | if !__unfmt_byte_text.is_empty() { 246 | break 'unformat None; 247 | } 248 | } 249 | } else { 250 | quote! { #(#captures)* } 251 | }; 252 | 253 | TokenStream::from(quote! { 254 | 'unformat: { 255 | use ::core::str::FromStr; 256 | use ::unfmt::bstr::{ByteSlice, BStr}; 257 | let Some((__unfmt_left, mut __unfmt_byte_text)) = BStr::new(#text).split_once_str(#initial_part) else { 258 | break 'unformat None; 259 | }; 260 | #capture_block 261 | Some((#(#capture_idents),*)) 262 | } 263 | }) 264 | } 265 | 266 | fn compile(pattern: &[u8], is_pattern_str: bool) -> (Vec, Vec) { 267 | let mut pattern = pattern.replace(b"{{", "\u{f8fd}"); 268 | pattern.reverse(); 269 | let mut pattern = pattern.replace(b"}}", "\u{f8fe}"); 270 | pattern.reverse(); 271 | 272 | let mut pattern_parts = pattern.split_str("{"); 273 | 274 | // SAFETY: The first part is always present. 275 | let initial_part = unsafe { 276 | pattern_parts 277 | .next() 278 | .unwrap_unchecked() 279 | .replace("\u{f8fd}", "{") 280 | }; 281 | 282 | let mut current_index: u32 = 0; 283 | let mut compiled_pattern = Vec::new(); 284 | for pattern_part in pattern_parts { 285 | let (capture, text) = pattern_part 286 | .split_once_str("}") 287 | .expect("unmatched } in pattern"); 288 | let capture = capture 289 | .to_str() 290 | .expect("invalid UTF-8 in capture names") 291 | .to_owned(); 292 | let mut text = text.replace("\u{f8fd}", b"{"); 293 | text.reverse(); 294 | let mut text = text.replace("\u{f8fe}", b"}"); 295 | text.reverse(); 296 | compiled_pattern.push(Capture::new( 297 | &text, 298 | &capture, 299 | is_pattern_str, 300 | &mut current_index, 301 | )); 302 | } 303 | 304 | assert!( 305 | compiled_pattern.windows(2).all(|parts| parts 306 | .iter() 307 | .any(|&Capture { ref text, .. }| !text.is_empty())), 308 | "consecutive captures are not allowed" 309 | ); 310 | 311 | (initial_part, compiled_pattern) 312 | } 313 | --------------------------------------------------------------------------------