├── .circleci └── config.yml ├── .credo.exs ├── .formatter.exs ├── .github ├── ISSUE_TEMPLATE │ └── please--open-new-issues-in-membranefranework-membrane_core.md └── workflows │ ├── on_issue_opened.yaml │ └── on_pr_opened.yaml ├── .gitignore ├── LICENSE ├── README.md ├── lib ├── bunch.ex └── bunch │ ├── access.ex │ ├── binary.ex │ ├── code.ex │ ├── config.ex │ ├── enum.ex │ ├── kv_enum.ex │ ├── kv_list.ex │ ├── list.ex │ ├── macro.ex │ ├── map.ex │ ├── markdown.ex │ ├── math.ex │ ├── module.ex │ ├── retry.ex │ ├── short_ref.ex │ ├── struct.ex │ ├── type.ex │ └── typespec.ex ├── mix.exs ├── mix.lock └── test ├── bunch ├── access_test.exs ├── binary_test.exs ├── config_test.exs ├── enum_test.exs ├── kv_enum_test.exs ├── list_test.exs ├── macro_test.exs ├── map_test.exs ├── markdown_test.exs ├── math_test.exs ├── retry_test.exs ├── short_ref_test.exs └── typespec_test.exs ├── bunch_test.exs └── test_helper.exs /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | elixir: membraneframework/elixir@1 4 | 5 | workflows: 6 | version: 2 7 | build: 8 | jobs: 9 | - elixir/build_test 10 | - elixir/test 11 | - elixir/lint 12 | -------------------------------------------------------------------------------- /.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo and you are probably reading 2 | # this after creating it with `mix credo.gen.config`. 3 | # 4 | # If you find anything wrong or unclear in this file, please report an 5 | # issue on GitHub: https://github.com/rrrene/credo/issues 6 | # 7 | %{ 8 | # 9 | # You can have as many configs as you like in the `configs:` field. 10 | configs: [ 11 | %{ 12 | # 13 | # Run any config using `mix credo -C `. If no config name is given 14 | # "default" is used. 15 | # 16 | name: "default", 17 | # 18 | # These are the files included in the analysis: 19 | files: %{ 20 | # 21 | # You can give explicit globs or simply directories. 22 | # In the latter case `**/*.{ex,exs}` will be used. 23 | # 24 | included: [ 25 | "lib/", 26 | "src/", 27 | "test/", 28 | "web/", 29 | "apps/*/lib/", 30 | "apps/*/src/", 31 | "apps/*/test/", 32 | "apps/*/web/" 33 | ], 34 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] 35 | }, 36 | # 37 | # Load and configure plugins here: 38 | # 39 | plugins: [], 40 | # 41 | # If you create your own checks, you must specify the source files for 42 | # them here, so they can be loaded by Credo before running the analysis. 43 | # 44 | requires: [], 45 | # 46 | # If you want to enforce a style guide and need a more traditional linting 47 | # experience, you can change `strict` to `true` below: 48 | # 49 | strict: false, 50 | # 51 | # To modify the timeout for parsing files, change this value: 52 | # 53 | parse_timeout: 5000, 54 | # 55 | # If you want to use uncolored output by default, you can change `color` 56 | # to `false` below: 57 | # 58 | color: true, 59 | # 60 | # You can customize the parameters of any check by adding a second element 61 | # to the tuple. 62 | # 63 | # To disable a check put `false` as second element: 64 | # 65 | # {Credo.Check.Design.DuplicatedCode, false} 66 | # 67 | checks: [ 68 | # 69 | ## Consistency Checks 70 | # 71 | {Credo.Check.Consistency.ExceptionNames, []}, 72 | {Credo.Check.Consistency.LineEndings, []}, 73 | {Credo.Check.Consistency.ParameterPatternMatching, []}, 74 | {Credo.Check.Consistency.SpaceAroundOperators, []}, 75 | {Credo.Check.Consistency.SpaceInParentheses, []}, 76 | {Credo.Check.Consistency.TabsOrSpaces, []}, 77 | 78 | # 79 | ## Design Checks 80 | # 81 | # You can customize the priority of any check 82 | # Priority values are: `low, normal, high, higher` 83 | # 84 | {Credo.Check.Design.AliasUsage, 85 | [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, 86 | # You can also customize the exit_status of each check. 87 | # If you don't want TODO comments to cause `mix credo` to fail, just 88 | # set this value to 0 (zero). 89 | # 90 | {Credo.Check.Design.TagTODO, [exit_status: 0]}, 91 | {Credo.Check.Design.TagFIXME, []}, 92 | 93 | # 94 | ## Readability Checks 95 | # 96 | {Credo.Check.Readability.AliasOrder, [priority: :normal]}, 97 | {Credo.Check.Readability.FunctionNames, []}, 98 | {Credo.Check.Readability.LargeNumbers, []}, 99 | {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, 100 | {Credo.Check.Readability.ModuleAttributeNames, []}, 101 | {Credo.Check.Readability.ModuleDoc, []}, 102 | {Credo.Check.Readability.ModuleNames, []}, 103 | {Credo.Check.Readability.ParenthesesInCondition, []}, 104 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, parens: true}, 105 | {Credo.Check.Readability.PredicateFunctionNames, []}, 106 | {Credo.Check.Readability.PreferImplicitTry, []}, 107 | {Credo.Check.Readability.RedundantBlankLines, []}, 108 | {Credo.Check.Readability.Semicolons, []}, 109 | {Credo.Check.Readability.SpaceAfterCommas, []}, 110 | {Credo.Check.Readability.StringSigils, []}, 111 | {Credo.Check.Readability.TrailingBlankLine, []}, 112 | {Credo.Check.Readability.TrailingWhiteSpace, []}, 113 | {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, 114 | {Credo.Check.Readability.VariableNames, []}, 115 | {Credo.Check.Readability.WithSingleClause, false}, 116 | 117 | # 118 | ## Refactoring Opportunities 119 | # 120 | {Credo.Check.Refactor.CondStatements, []}, 121 | {Credo.Check.Refactor.CyclomaticComplexity, []}, 122 | {Credo.Check.Refactor.FunctionArity, []}, 123 | {Credo.Check.Refactor.LongQuoteBlocks, []}, 124 | {Credo.Check.Refactor.MapInto, false}, 125 | {Credo.Check.Refactor.MatchInCondition, []}, 126 | {Credo.Check.Refactor.NegatedConditionsInUnless, []}, 127 | {Credo.Check.Refactor.NegatedConditionsWithElse, []}, 128 | {Credo.Check.Refactor.Nesting, []}, 129 | {Credo.Check.Refactor.UnlessWithElse, []}, 130 | {Credo.Check.Refactor.WithClauses, []}, 131 | 132 | # 133 | ## Warnings 134 | # 135 | {Credo.Check.Warning.BoolOperationOnSameValues, []}, 136 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, 137 | {Credo.Check.Warning.IExPry, []}, 138 | {Credo.Check.Warning.IoInspect, []}, 139 | {Credo.Check.Warning.LazyLogging, false}, 140 | {Credo.Check.Warning.MixEnv, []}, 141 | {Credo.Check.Warning.OperationOnSameValues, []}, 142 | {Credo.Check.Warning.OperationWithConstantResult, []}, 143 | {Credo.Check.Warning.RaiseInsideRescue, []}, 144 | {Credo.Check.Warning.UnusedEnumOperation, []}, 145 | {Credo.Check.Warning.UnusedFileOperation, []}, 146 | {Credo.Check.Warning.UnusedKeywordOperation, []}, 147 | {Credo.Check.Warning.UnusedListOperation, []}, 148 | {Credo.Check.Warning.UnusedPathOperation, []}, 149 | {Credo.Check.Warning.UnusedRegexOperation, []}, 150 | {Credo.Check.Warning.UnusedStringOperation, []}, 151 | {Credo.Check.Warning.UnusedTupleOperation, []}, 152 | {Credo.Check.Warning.UnsafeExec, []}, 153 | 154 | # 155 | # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) 156 | 157 | # 158 | # Controversial and experimental checks (opt-in, just replace `false` with `[]`) 159 | # 160 | {Credo.Check.Readability.StrictModuleLayout, 161 | priority: :normal, order: ~w/shortdoc moduledoc behaviour use import require alias/a}, 162 | {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, 163 | {Credo.Check.Consistency.UnusedVariableNames, force: :meaningful}, 164 | {Credo.Check.Design.DuplicatedCode, false}, 165 | {Credo.Check.Readability.AliasAs, false}, 166 | {Credo.Check.Readability.MultiAlias, false}, 167 | {Credo.Check.Readability.Specs, []}, 168 | {Credo.Check.Readability.SinglePipe, false}, 169 | {Credo.Check.Readability.WithCustomTaggedTuple, false}, 170 | {Credo.Check.Refactor.ABCSize, false}, 171 | {Credo.Check.Refactor.AppendSingleItem, false}, 172 | {Credo.Check.Refactor.DoubleBooleanNegation, false}, 173 | {Credo.Check.Refactor.ModuleDependencies, false}, 174 | {Credo.Check.Refactor.NegatedIsNil, false}, 175 | {Credo.Check.Refactor.PipeChainStart, false}, 176 | {Credo.Check.Refactor.VariableRebinding, false}, 177 | {Credo.Check.Warning.LeakyEnvironment, false}, 178 | {Credo.Check.Warning.MapGetUnsafePass, false}, 179 | {Credo.Check.Warning.UnsafeToAtom, false} 180 | 181 | # 182 | # Custom checks can be created using `mix credo.gen.check`. 183 | # 184 | ] 185 | } 186 | ] 187 | } 188 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: [ 3 | "{lib,test,spec,config}/**/*.{ex,exs}", 4 | "*.exs" 5 | ] 6 | ] 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/please--open-new-issues-in-membranefranework-membrane_core.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Please, open new issues in membranefranework/membrane_core 3 | about: New issues related to this repo should be opened there 4 | title: "[DO NOT OPEN]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please, do not open this issue here. Open it in the [membrane_core](https://github.com/membraneframework/membrane_core) repository instead. 11 | 12 | Thanks for helping us grow :) 13 | -------------------------------------------------------------------------------- /.github/workflows/on_issue_opened.yaml: -------------------------------------------------------------------------------- 1 | name: 'Close issue when opened' 2 | on: 3 | issues: 4 | types: 5 | - opened 6 | jobs: 7 | close: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout membrane_core 11 | uses: actions/checkout@v3 12 | with: 13 | repository: membraneframework/membrane_core 14 | - name: Close issue 15 | uses: ./.github/actions/close_issue 16 | with: 17 | GITHUB_TOKEN: ${{ secrets.MEMBRANEFRAMEWORKADMIN_TOKEN }} 18 | ISSUE_URL: ${{ github.event.issue.html_url }} 19 | ISSUE_NUMBER: ${{ github.event.issue.number }} 20 | REPOSITORY: ${{ github.repository }} 21 | -------------------------------------------------------------------------------- /.github/workflows/on_pr_opened.yaml: -------------------------------------------------------------------------------- 1 | name: Add PR to Smackore project board, if the author is from outside Membrane Team 2 | on: 3 | pull_request_target: 4 | types: 5 | - opened 6 | jobs: 7 | maybe_add_to_project_board: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout membrane_core 11 | uses: actions/checkout@v3 12 | with: 13 | repository: membraneframework/membrane_core 14 | - name: Puts PR in "New PRs by community" column in the Smackore project, if the author is from outside Membrane Team 15 | uses: ./.github/actions/add_pr_to_smackore_board 16 | with: 17 | GITHUB_TOKEN: ${{ secrets.MEMBRANEFRAMEWORKADMIN_TOKEN }} 18 | AUTHOR_LOGIN: ${{ github.event.pull_request.user.login }} 19 | PR_URL: ${{ github.event.pull_request.html_url }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .elixir_ls 2 | 3 | # Created by https://www.gitignore.io/api/c,vim,linux,macos,elixir,windows,visualstudiocode 4 | 5 | ### C ### 6 | # Prerequisites 7 | *.d 8 | 9 | # Object files 10 | *.o 11 | *.ko 12 | *.obj 13 | *.elf 14 | 15 | # Linker output 16 | *.ilk 17 | *.map 18 | *.exp 19 | 20 | # Precompiled Headers 21 | *.gch 22 | *.pch 23 | 24 | # Libraries 25 | *.lib 26 | *.a 27 | *.la 28 | *.lo 29 | 30 | # Shared objects (inc. Windows DLLs) 31 | *.dll 32 | *.so 33 | *.so.* 34 | *.dylib 35 | 36 | # Executables 37 | *.exe 38 | *.out 39 | *.app 40 | *.i*86 41 | *.x86_64 42 | *.hex 43 | 44 | # Debug files 45 | *.dSYM/ 46 | *.su 47 | *.idb 48 | *.pdb 49 | 50 | # Kernel Module Compile Results 51 | *.mod* 52 | *.cmd 53 | .tmp_versions/ 54 | modules.order 55 | Module.symvers 56 | Mkfile.old 57 | dkms.conf 58 | 59 | ### Elixir ### 60 | /_build 61 | /cover 62 | /deps 63 | /doc 64 | /.fetch 65 | erl_crash.dump 66 | *.ez 67 | *.beam 68 | 69 | ### Elixir Patch ### 70 | ### Linux ### 71 | *~ 72 | 73 | # temporary files which can be created if a process still has a handle open of a deleted file 74 | .fuse_hidden* 75 | 76 | # KDE directory preferences 77 | .directory 78 | 79 | # Linux trash folder which might appear on any partition or disk 80 | .Trash-* 81 | 82 | # .nfs files are created when an open file is removed but is still being accessed 83 | .nfs* 84 | 85 | ### macOS ### 86 | *.DS_Store 87 | .AppleDouble 88 | .LSOverride 89 | 90 | # Icon must end with two \r 91 | Icon 92 | 93 | # Thumbnails 94 | ._* 95 | 96 | # Files that might appear in the root of a volume 97 | .DocumentRevisions-V100 98 | .fseventsd 99 | .Spotlight-V100 100 | .TemporaryItems 101 | .Trashes 102 | .VolumeIcon.icns 103 | .com.apple.timemachine.donotpresent 104 | 105 | # Directories potentially created on remote AFP share 106 | .AppleDB 107 | .AppleDesktop 108 | Network Trash Folder 109 | Temporary Items 110 | .apdisk 111 | 112 | ### Vim ### 113 | # swap 114 | .sw[a-p] 115 | .*.sw[a-p] 116 | # session 117 | Session.vim 118 | # temporary 119 | .netrwhist 120 | # auto-generated tag files 121 | tags 122 | 123 | ### VisualStudioCode ### 124 | .vscode/* 125 | !.vscode/settings.json 126 | !.vscode/tasks.json 127 | !.vscode/launch.json 128 | !.vscode/extensions.json 129 | .history 130 | 131 | ### Windows ### 132 | # Windows thumbnail cache files 133 | Thumbs.db 134 | ehthumbs.db 135 | ehthumbs_vista.db 136 | 137 | # Folder config file 138 | Desktop.ini 139 | 140 | # Recycle Bin used on file shares 141 | $RECYCLE.BIN/ 142 | 143 | # Windows Installer files 144 | *.cab 145 | *.msi 146 | *.msm 147 | *.msp 148 | 149 | # Windows shortcuts 150 | *.lnk 151 | 152 | 153 | # End of https://www.gitignore.io/api/c,vim,linux,macos,elixir,windows,visualstudiocode 154 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 2018 Software Mansion 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bunch 2 | 3 | [![Hex.pm](https://img.shields.io/hexpm/v/bunch.svg)](https://hex.pm/packages/bunch) 4 | [![API Docs](https://img.shields.io/badge/api-docs-yellow.svg?style=flat)](https://hexdocs.pm/bunch/) 5 | [![CircleCI](https://circleci.com/gh/membraneframework/bunch.svg?style=svg)](https://circleci.com/gh/membraneframework/bunch) 6 | 7 | Bunch is a bunch of helper functions, intended to make life easier. 8 | 9 | API documentation is available at [HexDocs](https://hexdocs.pm/bunch/) 10 | 11 | ## Installation 12 | 13 | Add the following line to your `deps` in `mix.exs`. Run `mix deps.get`. 14 | 15 | ```elixir 16 | {:bunch, "~> 1.6"} 17 | ``` 18 | 19 | ## Copyright and License 20 | 21 | Copyright 2018, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 22 | 23 | [![Software Mansion](https://logo.swmansion.com/logo?color=white&variant=desktop&width=200&tag=membrane-github)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 24 | 25 | Licensed under the [Apache License, Version 2.0](LICENSE) 26 | -------------------------------------------------------------------------------- /lib/bunch.ex: -------------------------------------------------------------------------------- 1 | defmodule Bunch do 2 | @moduledoc """ 3 | A bunch of general-purpose helper and convenience functions. 4 | """ 5 | 6 | alias __MODULE__.Type 7 | 8 | @doc """ 9 | Imports a bunch of Bunch macros: `withl/1`, `withl/2`, `~>/2`, `~>>/2`, `quote_expr/1`, `quote_expr/2` 10 | """ 11 | defmacro __using__(_args) do 12 | quote do 13 | import unquote(__MODULE__), 14 | only: [ 15 | withl: 1, 16 | withl: 2, 17 | ~>: 2, 18 | ~>>: 2, 19 | quote_expr: 1, 20 | quote_expr: 2, 21 | then_if: 3 22 | ] 23 | end 24 | end 25 | 26 | @compile {:inline, listify: 1, error_if_nil: 2} 27 | 28 | @doc """ 29 | Extracts the key from a key-value tuple. 30 | """ 31 | @spec key({key, value}) :: key when key: any, value: any 32 | def key({key, _value}), do: key 33 | 34 | @doc """ 35 | Extracts the value from a key-value tuple. 36 | """ 37 | @spec value({key, value}) :: value when key: any, value: any 38 | def value({_key, value}), do: value 39 | 40 | @doc """ 41 | Creates a short reference. 42 | """ 43 | @spec make_short_ref() :: Bunch.ShortRef.t() 44 | defdelegate make_short_ref, to: Bunch.ShortRef, as: :new 45 | 46 | @doc """ 47 | Works like `quote/2`, but doesn't require a do/end block and options are passed 48 | as the last argument. 49 | 50 | Useful when quoting a single expression. 51 | 52 | ## Examples 53 | 54 | iex> use Bunch 55 | iex> quote_expr(String.t()) 56 | quote do String.t() end 57 | iex> quote_expr(unquote(x) + 2, unquote: false) 58 | quote unquote: false do unquote(x) + 2 end 59 | 60 | ## Nesting 61 | Nesting calls to `quote` disables unquoting in the inner call, while placing 62 | `quote_expr` in `quote` or another `quote_expr` does not: 63 | 64 | iex> use Bunch 65 | iex> quote do quote do unquote(:code) end end == quote do quote do :code end end 66 | false 67 | iex> quote do quote_expr(unquote(:code)) end == quote do quote_expr(:code) end 68 | true 69 | 70 | """ 71 | defmacro quote_expr(code, opts \\ []) do 72 | {:quote, [], [opts, [do: code]]} 73 | end 74 | 75 | @doc """ 76 | A labeled version of the `with/1` macro. 77 | 78 | This macro works like `with/1`, but enforces user to mark corresponding `withl` 79 | and `else` clauses with the same label (atom). If a `withl` clause does not 80 | match, only the `else` clauses marked with the same label are matched against 81 | the result. 82 | 83 | 84 | iex> use #{inspect(__MODULE__)} 85 | iex> names = %{1 => "Harold", 2 => "Małgorzata"} 86 | iex> test = fn id -> 87 | ...> withl id: {int_id, _} <- Integer.parse(id), 88 | ...> name: {:ok, name} <- Map.fetch(names, int_id) do 89 | ...> {:ok, "The name is \#{name}"} 90 | ...> else 91 | ...> id: :error -> {:error, :invalid_id} 92 | ...> name: :error -> {:error, :name_not_found} 93 | ...> end 94 | ...> end 95 | iex> test.("1") 96 | {:ok, "The name is Harold"} 97 | iex> test.("5") 98 | {:error, :name_not_found} 99 | iex> test.("something") 100 | {:error, :invalid_id} 101 | 102 | 103 | `withl` clauses using no `<-` operator are supported, but they also have to be 104 | labeled due to Elixir syntax restrictions. 105 | 106 | 107 | iex> use #{inspect(__MODULE__)} 108 | iex> names = %{1 => "Harold", 2 => "Małgorzata"} 109 | iex> test = fn id -> 110 | ...> withl id: {int_id, _} <- Integer.parse(id), 111 | ...> do: int_id = int_id + 1, 112 | ...> name: {:ok, name} <- Map.fetch(names, int_id) do 113 | ...> {:ok, "The name is \#{name}"} 114 | ...> else 115 | ...> id: :error -> {:error, :invalid_id} 116 | ...> name: :error -> {:error, :name_not_found} 117 | ...> end 118 | ...> end 119 | iex> test.("0") 120 | {:ok, "The name is Harold"} 121 | 122 | 123 | All the `withl` clauses that use `<-` operator must have at least one corresponding 124 | `else` clause. 125 | 126 | 127 | iex> use #{inspect(__MODULE__)} 128 | iex> try do 129 | ...> Code.compile_quoted(quote do 130 | ...> withl a: a when a > 0 <- 1, 131 | ...> b: b when b > 0 <- 2 do 132 | ...> {:ok, a + b} 133 | ...> else 134 | ...> a: _ -> :error 135 | ...> end 136 | ...> end) 137 | ...> rescue 138 | ...> e -> e.description 139 | ...> end 140 | "Label :b not present in withl else clauses" 141 | 142 | 143 | ## Variable scoping 144 | 145 | Because the labels are resolved in the compile time, they make it possible to 146 | access results of already succeeded matches from `else` clauses. This may help 147 | handling errors, like below: 148 | 149 | 150 | iex> use #{inspect(__MODULE__)} 151 | iex> names = %{1 => "Harold", 2 => "Małgorzata"} 152 | iex> test = fn id -> 153 | ...> withl id: {int_id, _} <- Integer.parse(id), 154 | ...> do: int_id = int_id + 1, 155 | ...> name: {:ok, name} <- Map.fetch(names, int_id) do 156 | ...> {:ok, "The name is \#{name}"} 157 | ...> else 158 | ...> id: :error -> {:error, :invalid_id} 159 | ...> name: :error -> {:ok, "The name is Defaultius the \#{int_id}th"} 160 | ...> end 161 | ...> end 162 | iex> test.("0") 163 | {:ok, "The name is Harold"} 164 | iex> test.("5") 165 | {:ok, "The name is Defaultius the 6th"} 166 | 167 | 168 | ## Duplicate labels 169 | 170 | `withl` supports marking multiple `withl` clauses with the same label, however 171 | in that case all the `else` clauses marked with such label are simply put multiple 172 | times into the generated code. Note that this may lead to confusion, in particular 173 | when variables are rebound in `withl` clauses: 174 | 175 | 176 | iex> use #{inspect(__MODULE__)} 177 | iex> test = fn x -> 178 | ...> withl a: x when x > 1 <- x, 179 | ...> do: x = x + 1, 180 | ...> a: x when x < 4 <- x do 181 | ...> :ok 182 | ...> else 183 | ...> a: x -> {:error, x} 184 | ...> end 185 | ...> end 186 | iex> test.(2) 187 | :ok 188 | iex> test.(1) 189 | {:error, 1} 190 | iex> test.(3) 191 | {:error, 4} 192 | 193 | 194 | """ 195 | @spec withl(keyword(with_clause :: term), do: code_block :: term(), else: match_clauses :: term) :: 196 | term 197 | defmacro withl(with_clauses, do: block, else: else_clauses) 198 | when is_list(with_clauses) and is_list(else_clauses) do 199 | do_withl(with_clauses, block, else_clauses, __CALLER__) 200 | end 201 | 202 | @doc """ 203 | Works like `withl/2`, but allows shorter syntax. 204 | 205 | ## Examples 206 | 207 | iex> use #{inspect(__MODULE__)} 208 | iex> x = 1 209 | iex> y = 2 210 | iex> withl a: true <- x > 0, 211 | ...> b: false <- y |> rem(2) == 0, 212 | ...> do: {x, y}, 213 | ...> else: (a: false -> {:error, :x}; b: true -> {:error, :y}) 214 | {:error, :y} 215 | 216 | 217 | For more details and more verbose and readable syntax, check docs for `withl/2`. 218 | """ 219 | @spec withl( 220 | keyword :: [ 221 | {key :: atom(), with_clause :: term} 222 | | {:do, code_block :: term} 223 | | {:else, match_clauses :: term} 224 | ] 225 | ) :: term 226 | defmacro withl(keyword) when is_list(keyword) do 227 | {{:else, else_clauses}, keyword} = keyword |> List.pop_at(-1) 228 | {{:do, block}, keyword} = keyword |> List.pop_at(-1) 229 | with_clauses = keyword 230 | do_withl(with_clauses, block, else_clauses, __CALLER__) 231 | end 232 | 233 | defp do_withl(with_clauses, block, else_clauses, caller) do 234 | else_clauses = 235 | else_clauses 236 | |> Enum.map(fn {:->, meta, [[[{label, left}]], right]} -> 237 | {label, {:->, meta, [[left], right]}} 238 | end) 239 | |> Enum.group_by(fn {k, _v} -> k end, fn {_k, v} -> v end) 240 | 241 | {result, used_labels} = 242 | with_clauses 243 | |> Enum.reverse() 244 | |> Enum.reduce({block, []}, fn 245 | {label, {:<-, meta, _args} = clause}, {acc, used_labels} -> 246 | label_else_clauses = 247 | else_clauses[label] || 248 | "Label #{inspect(label)} not present in withl else clauses" 249 | |> raise_compile_error(caller, meta) 250 | 251 | {{:with, meta, [clause, [do: acc, else: label_else_clauses]]}, [label | used_labels]} 252 | 253 | {_label, clause}, {acc, used_labels} -> 254 | {quote do 255 | unquote(clause) 256 | unquote(acc) 257 | end, used_labels} 258 | end) 259 | 260 | unused_else_clauses = Map.drop(else_clauses, used_labels) 261 | 262 | Enum.each(unused_else_clauses, fn {label, clauses} -> 263 | Enum.each(clauses, fn {:->, meta, _args} -> 264 | log_compile_warning( 265 | "withl's else clause labelled #{inspect(label)} will never match", 266 | caller, 267 | meta 268 | ) 269 | end) 270 | end) 271 | 272 | result 273 | end 274 | 275 | @doc """ 276 | Embeds the argument in a one-element list if it is not a list itself. Otherwise 277 | works as identity. 278 | 279 | Works similarly to `List.wrap/1`, but treats `nil` as any non-list value, 280 | instead of returning empty list in this case. 281 | 282 | ## Examples 283 | 284 | iex> #{inspect(__MODULE__)}.listify(:a) 285 | [:a] 286 | iex> #{inspect(__MODULE__)}.listify([:a, :b, :c]) 287 | [:a, :b, :c] 288 | iex> #{inspect(__MODULE__)}.listify(nil) 289 | [nil] 290 | 291 | """ 292 | @spec listify(a | [a]) :: [a] when a: any 293 | def listify(list) when is_list(list) do 294 | list 295 | end 296 | 297 | def listify(non_list) do 298 | [non_list] 299 | end 300 | 301 | @doc """ 302 | Returns an `:error` tuple if given value is `nil` and `:ok` tuple otherwise. 303 | 304 | ## Examples 305 | 306 | iex> map = %{:answer => 42} 307 | iex> #{inspect(__MODULE__)}.error_if_nil(map[:answer], :reason) 308 | {:ok, 42} 309 | iex> #{inspect(__MODULE__)}.error_if_nil(map[:invalid], :reason) 310 | {:error, :reason} 311 | 312 | """ 313 | @spec error_if_nil(value, reason) :: Type.try_t(value) 314 | when value: any(), reason: any() 315 | def error_if_nil(nil, reason), do: {:error, reason} 316 | def error_if_nil(v, _reason), do: {:ok, v} 317 | 318 | @doc """ 319 | Returns given stateful try value along with its status. 320 | """ 321 | @spec stateful_try_with_status(result) :: {status, result} 322 | when status: Type.try_t(), 323 | result: 324 | Type.stateful_try_t(state :: any) | Type.stateful_try_t(value :: any, state :: any) 325 | def stateful_try_with_status({:ok, _state} = res), do: {:ok, res} 326 | def stateful_try_with_status({{:ok, _res}, _state} = res), do: {:ok, res} 327 | def stateful_try_with_status({{:error, reason}, _state} = res), do: {{:error, reason}, res} 328 | 329 | @doc """ 330 | Helper for writing pipeline-like syntax. Maps given value using match clauses 331 | or lambda-like syntax. 332 | 333 | ## Examples 334 | 335 | iex> use #{inspect(__MODULE__)} 336 | iex> {:ok, 10} ~> ({:ok, x} -> x) 337 | 10 338 | iex> 5 ~> &1 + 2 339 | 7 340 | 341 | Lambda-like expressions are not converted to lambdas under the hood, but 342 | result of `expr` is injected to `&1` at the compile time. 343 | 344 | Useful especially when dealing with a pipeline of operations (made up e.g. 345 | with pipe (`|>`) operator) some of which are hard to express in such form: 346 | 347 | iex> use #{inspect(__MODULE__)} 348 | iex> ["Joe", "truck", "jacket"] 349 | ...> |> Enum.map(&String.downcase/1) 350 | ...> |> Enum.filter(& &1 |> String.starts_with?("j")) 351 | ...> ~> ["Words:" | &1] 352 | ...> |> Enum.join("\\n") 353 | "Words: 354 | joe 355 | jacket" 356 | 357 | """ 358 | # Case when the mapper is a list of match clauses 359 | defmacro expr ~> ([{:->, _, _} | _] = mapper) do 360 | quote do 361 | case unquote(expr) do 362 | unquote(mapper) 363 | end 364 | end 365 | end 366 | 367 | # Case when the mapper is a piece of lambda-like code 368 | defmacro expr ~> mapper do 369 | {mapped, arg_present?} = 370 | mapper 371 | |> Macro.prewalk(false, fn 372 | {:&, _meta, [1]}, _acc -> 373 | quote do: {expr_result, true} 374 | 375 | {:&, _meta, [i]} = node, acc when is_integer(i) -> 376 | {node, acc} 377 | 378 | {:&, meta, _args}, _acc -> 379 | """ 380 | The `&` (capture) operator is not allowed in lambda-like version of \ 381 | `#{inspect(__MODULE__)}.~>/2`. Use `&1` alone instead. 382 | """ 383 | |> raise_compile_error(__CALLER__, meta) 384 | 385 | other, acc -> 386 | {other, acc} 387 | end) 388 | 389 | if not arg_present? do 390 | """ 391 | `#{inspect(__MODULE__)}.~>/2` operator requires either match clauses or \ 392 | at least one occurrence of `&1` argument on the right hand side. 393 | """ 394 | |> raise_compile_error(__CALLER__) 395 | end 396 | 397 | quote do 398 | expr_result = unquote(expr) 399 | unquote(mapped) 400 | end 401 | end 402 | 403 | @doc """ 404 | Works similar to `~>/2`, but accepts only `->` clauses and appends default 405 | identity clause at the end. 406 | 407 | ## Examples 408 | 409 | iex> use #{inspect(__MODULE__)} 410 | iex> {:ok, 10} ~>> ({:ok, x} -> {:ok, x+1}) 411 | {:ok, 11} 412 | iex> :error ~>> ({:ok, x} -> {:ok, x+1}) 413 | :error 414 | 415 | """ 416 | defmacro expr ~>> ([{:->, _, _} | _] = mapper_clauses) do 417 | default = 418 | quote do 419 | default_result -> default_result 420 | end 421 | 422 | quote do 423 | case unquote(expr) do 424 | unquote(mapper_clauses ++ default) 425 | end 426 | end 427 | end 428 | 429 | defmacro _expr ~>> _ do 430 | """ 431 | `#{inspect(__MODULE__)}.~>>/2` operator expects match clauses on the right \ 432 | hand side. 433 | """ 434 | |> raise_compile_error(__CALLER__) 435 | end 436 | 437 | @spec raise_compile_error(term(), Macro.Env.t(), Keyword.t()) :: no_return() 438 | @spec raise_compile_error(term(), Macro.Env.t()) :: no_return() 439 | defp raise_compile_error(reason, caller, meta \\ []) do 440 | raise CompileError, 441 | file: caller.file, 442 | line: meta |> Keyword.get(:line, caller.line), 443 | description: reason 444 | end 445 | 446 | defp log_compile_warning(warning, caller, meta) do 447 | stacktrace = 448 | caller |> Map.update!(:line, &Keyword.get(meta, :line, &1)) |> Macro.Env.stacktrace() 449 | 450 | IO.warn(warning, stacktrace) 451 | end 452 | 453 | @doc """ 454 | Maps a value `x` with a `function` if the condition is true, acts as 455 | an identity function otherwise. 456 | 457 | ## Examples 458 | 459 | iex> use #{inspect(__MODULE__)} 460 | iex> then_if(1, false, & &1 + 1) 461 | 1 462 | iex> then_if(1, true, & &1 + 1) 463 | 2 464 | iex> arg = 1 465 | iex> arg |> then_if(not is_list(arg), fn arg -> [arg] end) |> Enum.map(&(&1*2)) 466 | [2] 467 | 468 | """ 469 | @spec then_if(x, condition :: boolean(), f :: (x -> y)) :: y when x: any(), y: any() 470 | def then_if(x, condition, f) do 471 | if condition do 472 | f.(x) 473 | else 474 | x 475 | end 476 | end 477 | end 478 | -------------------------------------------------------------------------------- /lib/bunch/access.ex: -------------------------------------------------------------------------------- 1 | defmodule Bunch.Access do 2 | @moduledoc """ 3 | A bunch of functions for easier manipulation on terms of types implementing `Access` 4 | behaviour. 5 | """ 6 | 7 | use Bunch 8 | 9 | import Kernel, except: [get_in: 2, put_in: 2, update_in: 3, get_and_update_in: 3, pop_in: 2] 10 | 11 | @compile {:inline, map_keys: 1} 12 | 13 | @gen_common_docs fn fun_name -> 14 | """ 15 | Works like `Kernel.#{fun_name}` with small differences. 16 | 17 | Behaviour differs in the following aspects: 18 | - empty lists of keys are allowed 19 | - single key does not have to be wrapped in a list 20 | """ 21 | end 22 | 23 | @doc """ 24 | Implements `Access` behaviour by delegating callbacks to `Map` module. 25 | 26 | All the callbacks are overridable. 27 | """ 28 | defmacro __using__(_args) do 29 | quote do 30 | @behaviour Access 31 | 32 | @impl true 33 | defdelegate fetch(term, key), to: Map 34 | 35 | @impl true 36 | defdelegate get_and_update(data, key, list), to: Map 37 | 38 | @impl true 39 | defdelegate pop(data, key), to: Map 40 | 41 | defoverridable Access 42 | end 43 | end 44 | 45 | @doc """ 46 | #{@gen_common_docs.("get_in/2")} 47 | 48 | ## Examples 49 | 50 | iex> #{inspect(__MODULE__)}.get_in(%{a: %{b: 10}}, [:a, :b]) 51 | 10 52 | iex> #{inspect(__MODULE__)}.get_in(%{a: 10}, :a) 53 | 10 54 | iex> #{inspect(__MODULE__)}.get_in(%{a: %{b: 10}}, []) 55 | %{a: %{b: 10}} 56 | 57 | """ 58 | @spec get_in(Access.t(), Access.key() | [Access.key()]) :: Access.value() 59 | def get_in(container, []), do: container 60 | def get_in(container, keys), do: container |> Kernel.get_in(keys |> map_keys) 61 | 62 | @doc """ 63 | #{@gen_common_docs.("put_in/3")} 64 | 65 | ## Examples 66 | 67 | iex> #{inspect(__MODULE__)}.put_in(%{a: %{b: 10}}, [:a, :b], 20) 68 | %{a: %{b: 20}} 69 | iex> #{inspect(__MODULE__)}.put_in(%{a: 10}, :a, 20) 70 | %{a: 20} 71 | iex> #{inspect(__MODULE__)}.put_in(%{a: %{b: 10}}, [], 20) 72 | 20 73 | 74 | """ 75 | @spec put_in(Access.t(), Access.key() | [Access.key()], Access.value()) :: Access.value() 76 | def put_in(_map, [], v), do: v 77 | def put_in(container, keys, v), do: container |> Kernel.put_in(keys |> map_keys, v) 78 | 79 | @doc """ 80 | #{@gen_common_docs.("update_in/3")} 81 | 82 | ## Examples 83 | 84 | iex> #{inspect(__MODULE__)}.update_in(%{a: %{b: 10}}, [:a, :b], & &1 * 2) 85 | %{a: %{b: 20}} 86 | iex> #{inspect(__MODULE__)}.update_in(%{a: 10}, :a, & &1 * 2) 87 | %{a: 20} 88 | iex> #{inspect(__MODULE__)}.update_in(10, [], & &1 * 2) 89 | 20 90 | 91 | """ 92 | @spec update_in(Access.t(), Access.key() | [Access.key()], (Access.value() -> Access.value())) :: 93 | Access.t() 94 | def update_in(container, [], f), do: f.(container) 95 | def update_in(container, keys, f), do: container |> Kernel.update_in(keys |> map_keys, f) 96 | 97 | @doc """ 98 | #{@gen_common_docs.("get_and_update_in/3")} 99 | 100 | ## Examples 101 | 102 | iex> #{inspect(__MODULE__)}.get_and_update_in(%{a: %{b: 10}}, [:a, :b], & {&1, &1 * 2}) 103 | {10, %{a: %{b: 20}}} 104 | iex> #{inspect(__MODULE__)}.get_and_update_in(%{a: 10}, :a, & {&1, &1 * 2}) 105 | {10, %{a: 20}} 106 | iex> #{inspect(__MODULE__)}.get_and_update_in(10, [], & {&1, &1 * 2}) 107 | {10, 20} 108 | 109 | """ 110 | @spec get_and_update_in(Access.t(), Access.key() | [Access.key()], (a -> {b, a})) :: 111 | {b, Access.t()} 112 | when a: Access.value(), b: any 113 | def get_and_update_in(container, [], f), do: f.(container) 114 | 115 | def get_and_update_in(container, keys, f), 116 | do: container |> Kernel.get_and_update_in(keys |> map_keys, f) 117 | 118 | @doc """ 119 | Updates value at `keys` in a nested data structure and returns new value and updated structure. 120 | 121 | Uses `get_and_update_in/3` under the hood. 122 | 123 | ## Example 124 | 125 | iex> %{a: %{b: 10}} |> #{inspect(__MODULE__)}.get_updated_in([:a, :b], & &1+1) 126 | {11, %{a: %{b: 11}}} 127 | 128 | """ 129 | @spec get_updated_in(Access.t(), Access.key() | [Access.key()], (Access.value() -> a)) :: 130 | {a, Access.t()} 131 | when a: Access.value() 132 | def get_updated_in(container, keys, f), 133 | do: container |> get_and_update_in(keys, fn a -> f.(a) ~> {&1, &1} end) 134 | 135 | @doc """ 136 | #{@gen_common_docs.("pop_in/2")} 137 | 138 | ## Examples 139 | 140 | iex> #{inspect(__MODULE__)}.pop_in(%{a: %{b: 10}}, [:a, :b]) 141 | {10, %{a: %{}}} 142 | iex> #{inspect(__MODULE__)}.pop_in(%{a: 10}, :a) 143 | {10, %{}} 144 | iex> #{inspect(__MODULE__)}.pop_in(10, []) 145 | {10, nil} 146 | 147 | """ 148 | @spec pop_in(Access.t(), Access.key() | [Access.key()]) :: {Access.value(), Access.t()} 149 | def pop_in(container, []), do: {container, nil} 150 | def pop_in(container, keys), do: container |> Kernel.pop_in(keys |> map_keys) 151 | 152 | @doc """ 153 | Works like `pop_in/2`, but discards returned value. 154 | 155 | ## Examples 156 | 157 | iex> #{inspect(__MODULE__)}.delete_in(%{a: %{b: 10}}, [:a, :b]) 158 | %{a: %{}} 159 | iex> #{inspect(__MODULE__)}.delete_in(%{a: 10}, :a) 160 | %{} 161 | iex> #{inspect(__MODULE__)}.delete_in(10, []) 162 | nil 163 | 164 | """ 165 | @spec delete_in(Access.t(), Access.key() | [Access.key()]) :: Access.t() 166 | def delete_in(container, keys), do: pop_in(container, keys) ~> ({_out, container} -> container) 167 | 168 | @spec map_keys(Access.key() | [Access.key()]) :: [Access.key()] 169 | defp map_keys(keys), do: keys |> Bunch.listify() 170 | end 171 | -------------------------------------------------------------------------------- /lib/bunch/binary.ex: -------------------------------------------------------------------------------- 1 | defmodule Bunch.Binary do 2 | @moduledoc """ 3 | A bunch of helpers for manipulating binaries. 4 | """ 5 | 6 | use Bunch 7 | 8 | @doc """ 9 | Chunks given binary into parts of given size. 10 | 11 | Remaining part is cut off. 12 | 13 | ## Examples 14 | 15 | iex> <<1, 2, 3, 4, 5, 6>> |> #{inspect(__MODULE__)}.chunk_every(2) 16 | [<<1, 2>>, <<3, 4>>, <<5, 6>>] 17 | iex> <<1, 2, 3, 4, 5, 6, 7>> |> #{inspect(__MODULE__)}.chunk_every(2) 18 | [<<1, 2>>, <<3, 4>>, <<5, 6>>] 19 | 20 | """ 21 | @spec chunk_every(binary, pos_integer) :: [binary] 22 | def chunk_every(binary, chunk_size) do 23 | {result, _} = chunk_every_rem(binary, chunk_size) 24 | result 25 | end 26 | 27 | @doc """ 28 | Chunks given binary into parts of given size. 29 | 30 | Returns list of chunks and remainder. 31 | 32 | ## Examples 33 | 34 | iex> <<1, 2, 3, 4, 5, 6>> |> #{inspect(__MODULE__)}.chunk_every_rem(2) 35 | {[<<1, 2>>, <<3, 4>>, <<5, 6>>], <<>>} 36 | iex> <<1, 2, 3, 4, 5, 6, 7>> |> #{inspect(__MODULE__)}.chunk_every_rem(2) 37 | {[<<1, 2>>, <<3, 4>>, <<5, 6>>], <<7>>} 38 | 39 | """ 40 | @spec chunk_every_rem(binary, chunk_size :: pos_integer) :: {[binary], remainder :: binary} 41 | def chunk_every_rem(binary, chunk_size) do 42 | do_chunk_every_rem(binary, chunk_size) 43 | end 44 | 45 | defp do_chunk_every_rem(binary, chunk_size, acc \\ []) do 46 | case binary do 47 | <> <> rest -> 48 | do_chunk_every_rem(rest, chunk_size, [chunk | acc]) 49 | 50 | rest -> 51 | {acc |> Enum.reverse(), rest} 52 | end 53 | end 54 | 55 | @doc """ 56 | Cuts off the smallest possible chunk from the end of `binary`, so that the 57 | size of returned binary is an integer multiple of `i`. 58 | 59 | ## Examples 60 | 61 | iex> import #{inspect(__MODULE__)} 62 | iex> take_int_part(<<1,2,3,4,5,6,7,8>>, 3) 63 | <<1,2,3,4,5,6>> 64 | iex> take_int_part(<<1,2,3,4,5,6,7,8>>, 4) 65 | <<1,2,3,4,5,6,7,8>> 66 | 67 | """ 68 | @spec take_int_part(binary, pos_integer) :: binary 69 | def take_int_part(binary, i) do 70 | {b, _} = split_int_part(binary, i) 71 | b 72 | end 73 | 74 | @doc """ 75 | Returns a 2-tuple, where the first element is the result of `take_int_part(binary, i)`, 76 | and the second is the rest of `binary`. 77 | 78 | ## Examples 79 | 80 | iex> import #{inspect(__MODULE__)} 81 | iex> split_int_part(<<1,2,3,4,5,6,7,8>>, 3) 82 | {<<1,2,3,4,5,6>>, <<7,8>>} 83 | iex> split_int_part(<<1,2,3,4,5,6,7,8>>, 4) 84 | {<<1,2,3,4,5,6,7,8>>, <<>>} 85 | 86 | """ 87 | @spec split_int_part(binary, pos_integer) :: {binary, binary} 88 | def split_int_part(binary, i) do 89 | len = Bunch.Math.max_multiple_lte(i, binary |> byte_size()) 90 | <> = binary 91 | {b, r} 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/bunch/code.ex: -------------------------------------------------------------------------------- 1 | defmodule Bunch.Code do 2 | @moduledoc """ 3 | A bunch of helper functions for code compilation, code evaluation, and code loading. 4 | """ 5 | 6 | @doc """ 7 | Takes a code block, expands macros inside and pretty prints it. 8 | """ 9 | defmacro peek_code(do: block) do 10 | block 11 | |> Bunch.Macro.expand_deep(__CALLER__) 12 | |> Macro.to_string() 13 | |> IO.puts() 14 | 15 | block 16 | end 17 | 18 | @doc """ 19 | Returns stacktrace as a string. 20 | 21 | The stacktrace is formatted to the readable format. 22 | """ 23 | defmacro stacktrace do 24 | quote do 25 | {:current_stacktrace, trace} = Process.info(self(), :current_stacktrace) 26 | 27 | # drop excludes `Process.info/2` call 28 | trace 29 | |> Enum.drop(1) 30 | |> Exception.format_stacktrace() 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/bunch/config.ex: -------------------------------------------------------------------------------- 1 | defmodule Bunch.Config do 2 | @moduledoc """ 3 | A bunch of helpers for parsing and validating configurations. 4 | """ 5 | 6 | use Bunch 7 | 8 | alias Bunch.Type 9 | 10 | @doc """ 11 | Parses `config` according to `fields_specs`. 12 | 13 | `fields_specs` consist of constraints on each field. Supported constraints are: 14 | * validate - function determining if field's value is correct 15 | * in - enumerable containing all valid values 16 | * default - value returned if a field is not found in `config` 17 | * require? - determines whether a field is required, defaults to `false` when `default` is set 18 | and `true` when `default` is not set 19 | * require_if - deprecated, pass function returning constraints instead 20 | 21 | Instead of a list of constraints, a function accepting fields parsed so far and returning 22 | the constraints can be passed. If the function returns `nil`, field is considered non existent, 23 | as if it wasn't passed at all. 24 | 25 | ## Examples 26 | 27 | iex> #{inspect(__MODULE__)}.parse([a: 1, b: 2], a: [validate: & &1 > 0], b: [in: -2..2]) 28 | {:ok, %{a: 1, b: 2}} 29 | iex> #{inspect(__MODULE__)}.parse([a: 1, b: 4], a: [validate: & &1 > 0], b: [in: -2..2]) 30 | {:error, {:config_field, {:invalid_value, [key: :b, value: 4, reason: {:not_in, -2..2}]}}} 31 | iex> #{inspect(__MODULE__)}.parse( 32 | ...> [a: 1, b: 2], 33 | ...> a: [validate: & &1 > 0], 34 | ...> b: [in: -2..2], 35 | ...> c: [default: 5] 36 | ...> ) 37 | {:ok, %{a: 1, b: 2, c: 5}} 38 | iex> #{inspect(__MODULE__)}.parse( 39 | ...> [a: 1, b: 2], 40 | ...> a: [validate: & &1 > 0], 41 | ...> b: [in: -2..2], 42 | ...> c: [require?: false] 43 | ...> ) 44 | {:ok, %{a: 1, b: 2}} 45 | iex> #{inspect(__MODULE__)}.parse( 46 | ...> [a: 1, b: 1], 47 | ...> a: [validate: & &1 > 0], 48 | ...> b: [in: -2..2], 49 | ...> c: &(if &1.a == &1.b, do: [in: 0..1]) 50 | ...> ) 51 | {:error, {:config_field, {:key_not_found, :c}}} 52 | 53 | """ 54 | @spec parse( 55 | config :: Keyword.t(v), 56 | [field | {field, field_specs | (parsed_config -> field_specs)}] 57 | ) :: Type.try_t(parsed_config) 58 | when parsed_config: %{atom => v}, 59 | field: atom, 60 | v: any, 61 | field_specs: 62 | [ 63 | validate: 64 | (v | any -> Type.try_t() | boolean) 65 | | (v | any, parsed_config -> Type.try_t() | boolean), 66 | in: Enumerable.t(), 67 | default: v, 68 | require?: boolean, 69 | require_if: (parsed_config -> boolean) 70 | ] 71 | | nil 72 | def parse(config, fields_specs) do 73 | withl kw: true <- config |> Keyword.keyword?(), 74 | dup: [] <- config |> Keyword.keys() |> Bunch.Enum.duplicates(), 75 | do: config = config |> Map.new(), 76 | fields: 77 | {:ok, remaining_config, parsed_config} when remaining_config == %{} <- 78 | parse_fields(config, fields_specs, %{}) do 79 | {:ok, parsed_config} 80 | else 81 | kw: false -> 82 | {:error, {:config_not_keyword, config}} 83 | 84 | dup: duplicates -> 85 | {:error, {:config_duplicates, duplicates}} 86 | 87 | fields: {:error, reason} -> 88 | {:error, {:config_field, reason}} 89 | 90 | fields: {:ok, remaining_config, _parsed_config} -> 91 | {:error, {:config_invalid_keys, Map.keys(remaining_config)}} 92 | end 93 | end 94 | 95 | defp parse_fields(config, [], parsed_config) do 96 | {:ok, config, parsed_config} 97 | end 98 | 99 | defp parse_fields(config, [field_spec | fields_specs], parsed_config) do 100 | {key, spec} = 101 | case field_spec do 102 | key when is_atom(key) -> {key, []} 103 | {key, spec} when is_list(spec) -> {key, spec} 104 | {key, spec} when is_function(spec, 1) -> {key, spec.(parsed_config)} 105 | end 106 | 107 | if spec == nil do 108 | parse_fields(config, fields_specs, parsed_config) 109 | else 110 | case parse_field(key, Map.new(spec), Map.fetch(config, key), parsed_config) do 111 | {:ok, {key, value}} -> 112 | parse_fields(Map.delete(config, key), fields_specs, Map.put(parsed_config, key, value)) 113 | 114 | {:ok, :ignore} -> 115 | parse_fields(Map.delete(config, key), fields_specs, parsed_config) 116 | 117 | {:error, :field_not_accepted} -> 118 | parse_fields(config, fields_specs, parsed_config) 119 | 120 | {:error, reason} -> 121 | {:error, reason} 122 | end 123 | end 124 | end 125 | 126 | defp parse_field(key, %{require_if: require_if} = spec, value, config) do 127 | require Logger 128 | 129 | Logger.warning( 130 | "Passing :require_if option to Bunch.Config.parse/2 is deprecated, pass function returning constraints instead" 131 | ) 132 | 133 | spec = spec |> Map.delete(:require_if) 134 | 135 | cond do 136 | require_if.(config) -> 137 | parse_field(key, spec |> Map.delete(:default), value, config) 138 | 139 | Map.has_key?(spec, :default) -> 140 | parse_field(key, spec, value, config) 141 | 142 | true -> 143 | {:error, :field_not_accepted} 144 | end 145 | end 146 | 147 | defp parse_field(key, %{default: default}, :error, _config) do 148 | {:ok, {key, default}} 149 | end 150 | 151 | defp parse_field(_key, %{require?: false}, :error, _config) do 152 | {:ok, :ignore} 153 | end 154 | 155 | defp parse_field(key, _spec, :error, _config) do 156 | {:error, {:key_not_found, key}} 157 | end 158 | 159 | defp parse_field(key, spec, {:ok, value}, config) do 160 | validate = spec |> Map.get(:validate, fn _value -> :ok end) 161 | in_enum = spec |> Map.get(:in, [value]) 162 | 163 | withl fun: 164 | res when res in [:ok, true] <- 165 | (case Function.info(validate)[:arity] do 166 | 1 -> validate.(value) 167 | 2 -> validate.(value, config) 168 | end), 169 | enum: true <- value in in_enum do 170 | {:ok, {key, value}} 171 | else 172 | fun: false -> 173 | {:error, {:invalid_value, key: key, value: value}} 174 | 175 | fun: {:error, reason} -> 176 | {:error, {:invalid_value, key: key, value: value, reason: reason}} 177 | 178 | enum: false -> 179 | {:error, {:invalid_value, key: key, value: value, reason: {:not_in, in_enum}}} 180 | end 181 | end 182 | end 183 | -------------------------------------------------------------------------------- /lib/bunch/enum.ex: -------------------------------------------------------------------------------- 1 | defmodule Bunch.Enum do 2 | @moduledoc """ 3 | A bunch of helper functions for manipulating enums. 4 | """ 5 | 6 | use Bunch 7 | alias Bunch.Type 8 | 9 | @doc """ 10 | Generates a list consisting of `i` values `v`. 11 | 12 | 13 | iex> #{inspect(__MODULE__)}.repeated(:abc, 4) 14 | [:abc, :abc, :abc, :abc] 15 | iex> #{inspect(__MODULE__)}.repeated(:abc, 0) 16 | [] 17 | 18 | """ 19 | @spec repeated(v, non_neg_integer) :: [v] when v: any() 20 | def repeated(v, i) when i >= 0 do 21 | do_repeated(v, i, []) 22 | end 23 | 24 | defp do_repeated(_v, 0, acc) do 25 | acc 26 | end 27 | 28 | defp do_repeated(v, i, acc) do 29 | do_repeated(v, i - 1, [v | acc]) 30 | end 31 | 32 | @doc """ 33 | Generates a list by calling `i` times function `f`. 34 | 35 | 36 | iex> {:ok, pid} = Agent.start_link(fn -> 0 end) 37 | iex> #{inspect(__MODULE__)}.repeat(fn -> Agent.get_and_update(pid, &{&1, &1+1}) end, 4) 38 | [0, 1, 2, 3] 39 | iex> #{inspect(__MODULE__)}.repeat(fn -> :abc end, 0) 40 | [] 41 | 42 | """ 43 | @spec repeat(f :: (-> a), non_neg_integer) :: [a] when a: any() 44 | def repeat(fun, i) when i >= 0 do 45 | do_repeat(fun, i, []) 46 | end 47 | 48 | defp do_repeat(_fun, 0, acc) do 49 | acc |> Enum.reverse() 50 | end 51 | 52 | defp do_repeat(fun, i, acc) do 53 | do_repeat(fun, i - 1, [fun.() | acc]) 54 | end 55 | 56 | @doc """ 57 | Splits enumerable into chunks, and passes each chunk through `collector`. 58 | 59 | New chunk is created each time `chunker` returns `false`. The `chunker` is passed 60 | current and previous element of enumerable. 61 | 62 | ## Examples: 63 | 64 | iex> #{inspect(__MODULE__)}.chunk_by_prev([1,2,5,5], fn x, y -> x - y <= 2 end) 65 | [[1, 2], [5, 5]] 66 | iex> #{inspect(__MODULE__)}.chunk_by_prev([1,2,5,5], fn x, y -> x - y <= 2 end, &Enum.sum/1) 67 | [3, 10] 68 | 69 | """ 70 | @spec chunk_by_prev(Enum.t(), chunker :: (a, a -> boolean), collector :: ([a] -> b)) :: [b] 71 | when a: any(), b: any() 72 | def chunk_by_prev(enum, chunker, collector \\ & &1) do 73 | enum 74 | |> Enum.to_list() 75 | ~> ( 76 | [h | t] -> do_chunk_by_prev(t, chunker, collector, [[h]]) 77 | [] -> [] 78 | ) 79 | end 80 | 81 | defp do_chunk_by_prev([h | t], chunker, collector, [[lh | lt] | acc]) do 82 | do_chunk_by_prev( 83 | t, 84 | chunker, 85 | collector, 86 | if chunker.(h, lh) do 87 | [[h, lh | lt] | acc] 88 | else 89 | [[h], [lh | lt] |> Enum.reverse() |> collector.() | acc] 90 | end 91 | ) 92 | end 93 | 94 | defp do_chunk_by_prev([], _chunker, collector, [l | acc]) do 95 | [l |> Enum.reverse() |> collector.() | acc] |> Enum.reverse() 96 | end 97 | 98 | @doc """ 99 | Works like `Enum.reduce/3`, but breaks on error. 100 | 101 | Behaves like `Enum.reduce/3` as long as given `fun` returns `{:ok, new_acc}`. 102 | If it happens to return `{{:error, reason}, new_acc}`, reduction is stopped and 103 | the error is returned. 104 | 105 | ## Examples: 106 | 107 | iex> fun = fn 108 | ...> x, acc when acc >= 0 -> {:ok, x + acc} 109 | ...> _, acc -> {{:error, :negative_prefix_sum}, acc} 110 | ...> end 111 | iex> #{inspect(__MODULE__)}.try_reduce([1,5,-2,8], 0, fun) 112 | {:ok, 12} 113 | iex> #{inspect(__MODULE__)}.try_reduce([1,5,-7,8], 0, fun) 114 | {{:error, :negative_prefix_sum}, -1} 115 | 116 | """ 117 | @spec try_reduce(Enum.t(), acc, fun :: (a, acc -> result)) :: result 118 | when a: any(), acc: any(), result: Type.stateful_try_t(acc) 119 | def try_reduce(enum, acc, f) do 120 | Enum.reduce_while(enum, {:ok, acc}, fn e, {:ok, acc} -> 121 | case f.(e, acc) do 122 | {:ok, new_acc} -> {:cont, {:ok, new_acc}} 123 | {{:error, reason}, new_acc} -> {:halt, {{:error, reason}, new_acc}} 124 | end 125 | end) 126 | end 127 | 128 | @doc """ 129 | Works like `Enum.reduce_while/3`, but breaks on error. 130 | 131 | Behaves like `Enum.reduce_while/3` as long as given `fun` returns 132 | `{{:ok, :cont | :halt}, new_acc}`. If it happens to return 133 | `{{:error, reason}, new_acc}`, reduction is stopped and the error is returned. 134 | 135 | ## Examples: 136 | 137 | iex> fun = fn 138 | ...> 0, acc -> {{:ok, :halt}, acc} 139 | ...> x, acc when acc >= 0 -> {{:ok, :cont}, x + acc} 140 | ...> _, acc -> {{:error, :negative_prefix_sum}, acc} 141 | ...> end 142 | iex> #{inspect(__MODULE__)}.try_reduce_while([1,5,-2,8], 0, fun) 143 | {:ok, 12} 144 | iex> #{inspect(__MODULE__)}.try_reduce_while([1,5,0,8], 0, fun) 145 | {:ok, 6} 146 | iex> #{inspect(__MODULE__)}.try_reduce_while([1,5,-7,8], 0, fun) 147 | {{:error, :negative_prefix_sum}, -1} 148 | 149 | """ 150 | @spec try_reduce_while( 151 | Enum.t(), 152 | acc, 153 | reducer :: (a, acc -> Type.stateful_try_t(:cont | :halt, acc)) 154 | ) :: Type.stateful_try_t(acc) 155 | when a: any(), acc: any() 156 | def try_reduce_while(enum, acc, f) do 157 | Enum.reduce_while(enum, {:ok, acc}, fn e, {:ok, acc} -> 158 | case f.(e, acc) do 159 | {{:ok, :cont}, new_acc} -> {:cont, {:ok, new_acc}} 160 | {{:ok, :halt}, new_acc} -> {:halt, {:ok, new_acc}} 161 | {{:error, reason}, new_acc} -> {:halt, {{:error, reason}, new_acc}} 162 | end 163 | end) 164 | end 165 | 166 | @doc """ 167 | Works like `Enum.each/2`, but breaks on error. 168 | 169 | Behaves like `Enum.each/2` as long as given `fun` returns `:ok`. 170 | If it happens to return `{:error, reason}`, traversal is stopped and the 171 | error is returned. 172 | 173 | ## Examples: 174 | 175 | iex> fun = fn 0 -> {:error, :zero}; x -> send(self(), 1/x); :ok end 176 | iex> #{inspect(__MODULE__)}.try_each([1,2,3], fun) 177 | :ok 178 | iex> #{inspect(__MODULE__)}.try_each([1,0,3], fun) 179 | {:error, :zero} 180 | 181 | """ 182 | @spec try_each(Enum.t(), fun :: (a -> result)) :: result 183 | when a: any(), result: Type.try_t() 184 | def try_each(enum, f), do: do_try_each(enum |> Enum.to_list(), f) 185 | defp do_try_each([], _f), do: :ok 186 | 187 | defp do_try_each([h | t], f) do 188 | case f.(h) do 189 | :ok -> do_try_each(t, f) 190 | {:error, _error} = error -> error 191 | end 192 | end 193 | 194 | @doc """ 195 | Works like `Enum.map/2`, but breaks on error. 196 | 197 | Behaves like `Enum.map/2` as long as given `fun` returns `{:ok, value}`. 198 | If it happens to return `{:error, reason}`, reduction is stopped and the 199 | error is returned. 200 | 201 | ## Examples: 202 | 203 | iex> fun = fn 0 -> {:error, :zero}; x -> {:ok, 1/x} end 204 | iex> #{inspect(__MODULE__)}.try_map([1,5,-2,8], fun) 205 | {:ok, [1.0, 0.2, -0.5, 0.125]} 206 | iex> #{inspect(__MODULE__)}.try_map([1,5,0,8], fun) 207 | {:error, :zero} 208 | 209 | """ 210 | @spec try_map(Enum.t(), fun :: (a -> Type.try_t(b))) :: Type.try_t([b]) 211 | when a: any(), b: any() 212 | def try_map(enum, f), do: do_try_map(enum |> Enum.to_list(), f, []) 213 | defp do_try_map([], _f, acc), do: {:ok, acc |> Enum.reverse()} 214 | 215 | defp do_try_map([h | t], f, acc) do 216 | case f.(h) do 217 | {:ok, res} -> do_try_map(t, f, [res | acc]) 218 | {:error, reason} -> {:error, reason} 219 | end 220 | end 221 | 222 | @doc """ 223 | Works like `Enum.flat_map/2`, but breaks on error. 224 | 225 | Behaves like `Enum.flat_map/2` as long as reducing function returns `{:ok, values}`. 226 | If it happens to return `{:error, reason}`, reduction is stopped and the 227 | error is returned. 228 | 229 | ## Examples: 230 | 231 | iex> fun = fn 0 -> {:error, :zero}; x -> {:ok, [1/x, 2/x, 3/x]} end 232 | iex> #{inspect(__MODULE__)}.try_flat_map([1,5,-2,8], fun) 233 | {:ok, [1.0, 2.0, 3.0, 0.2, 0.4, 0.6, -0.5, -1.0, -1.5, 0.125, 0.25, 0.375]} 234 | iex> #{inspect(__MODULE__)}.try_flat_map([1,5,0,8], fun) 235 | {:error, :zero} 236 | 237 | """ 238 | @spec try_flat_map(Enum.t(), fun :: (a -> result)) :: result 239 | when a: any(), b: any(), result: Type.try_t([b]) 240 | def try_flat_map(enum, f), do: do_try_flat_map(enum |> Enum.to_list(), f, []) 241 | defp do_try_flat_map([], _f, acc), do: {:ok, acc |> Enum.reverse()} 242 | 243 | defp do_try_flat_map([h | t], f, acc) do 244 | case f.(h) do 245 | {:ok, res} -> do_try_flat_map(t, f, res |> Enum.reverse(acc)) 246 | {:error, reason} -> {:error, reason} 247 | end 248 | end 249 | 250 | @doc """ 251 | Works like `Enum.map_reduce/3`, but breaks on error. 252 | 253 | Behaves like `Enum.map_reduce/3` as long as given `fun` returns 254 | `{{:ok, value}, new_acc}`. If it happens to return `{{:error, reason}, new_acc}`, 255 | reduction is stopped and the error is returned. 256 | 257 | ## Examples: 258 | 259 | iex> fun = fn 260 | ...> x, acc when acc >= 0 -> {{:ok, x+1}, x + acc} 261 | ...> _, acc -> {{:error, :negative_prefix_sum}, acc} 262 | ...> end 263 | iex> #{inspect(__MODULE__)}.try_map_reduce([1,5,-2,8], 0, fun) 264 | {{:ok, [2,6,-1,9]}, 12} 265 | iex> #{inspect(__MODULE__)}.try_map_reduce([1,5,-7,8], 0, fun) 266 | {{:error, :negative_prefix_sum}, -1} 267 | 268 | """ 269 | @spec try_map_reduce(Enum.t(), acc, fun :: (a, acc -> Type.stateful_try_t(b, acc))) :: 270 | Type.stateful_try_t([b], acc) 271 | when a: any(), b: any(), acc: any() 272 | def try_map_reduce(enum, acc, f), do: do_try_map_reduce(enum |> Enum.to_list(), acc, f, []) 273 | defp do_try_map_reduce([], f_acc, _f, acc), do: {{:ok, acc |> Enum.reverse()}, f_acc} 274 | 275 | defp do_try_map_reduce([h | t], f_acc, f, acc) do 276 | case f.(h, f_acc) do 277 | {{:ok, res}, f_acc} -> do_try_map_reduce(t, f_acc, f, [res | acc]) 278 | {{:error, reason}, f_acc} -> {{:error, reason}, f_acc} 279 | end 280 | end 281 | 282 | @doc """ 283 | Works like `Enum.each/2`, but breaks on error. 284 | 285 | Behaves like `Enum.flat_map_reduce/3` as long as given `fun` returns 286 | `{{:ok, value}, new_acc}`. If it happens to return `{{:error, reason}, new_acc}`, 287 | reduction is stopped and the error is returned. 288 | 289 | ## Examples: 290 | 291 | iex> fun = fn 292 | ...> x, acc when acc >= 0 -> {{:ok, [x+1, x+2, x+3]}, x + acc} 293 | ...> _, acc -> {{:error, :negative_prefix_sum}, acc} 294 | ...> end 295 | iex> #{inspect(__MODULE__)}.try_flat_map_reduce([1,5,-2,8], 0, fun) 296 | {{:ok, [2,3,4,6,7,8,-1,0,1,9,10,11]}, 12} 297 | iex> #{inspect(__MODULE__)}.try_flat_map_reduce([1,5,-7,8], 0, fun) 298 | {{:error, :negative_prefix_sum}, -1} 299 | 300 | """ 301 | @spec try_flat_map_reduce(Enum.t(), acc, fun :: (a, acc -> result)) :: result 302 | when a: any(), b: any(), acc: any(), result: Type.stateful_try_t([b], acc) 303 | def try_flat_map_reduce(enum, acc, f), 304 | do: try_flat_map_reduce(enum |> Enum.to_list(), acc, f, []) 305 | 306 | defp try_flat_map_reduce([], f_acc, _f, acc), do: {{:ok, acc |> Enum.reverse()}, f_acc} 307 | 308 | defp try_flat_map_reduce([h | t], f_acc, f, acc) do 309 | case f.(h, f_acc) do 310 | {{:ok, res}, f_acc} -> try_flat_map_reduce(t, f_acc, f, (res |> Enum.reverse()) ++ acc) 311 | {{:error, reason}, f_acc} -> {{:error, reason}, f_acc} 312 | {:error, reason} -> {{:error, reason}, f_acc} 313 | end 314 | end 315 | 316 | @doc """ 317 | Works the same way as `Enum.zip/1`, but does not cut off remaining values. 318 | 319 | ## Examples: 320 | 321 | iex> #{inspect(__MODULE__)}.zip_longest([[1, 2] ,[3 ,4, 5]]) 322 | [[1, 3], [2, 4], [5]] 323 | 324 | It also returns list of lists, as opposed to tuples. 325 | """ 326 | @spec zip_longest(list()) :: list(list()) 327 | def zip_longest(lists) when is_list(lists) do 328 | zip_longest_recurse(lists, []) 329 | end 330 | 331 | defp zip_longest_recurse(lists, acc) do 332 | {lists, zipped} = 333 | lists 334 | |> Enum.reject(&Enum.empty?/1) 335 | |> Enum.map_reduce([], fn [h | t], acc -> {t, [h | acc]} end) 336 | 337 | if zipped |> Enum.empty?() do 338 | Enum.reverse(acc) 339 | else 340 | zipped = zipped |> Enum.reverse() 341 | zip_longest_recurse(lists, [zipped | acc]) 342 | end 343 | end 344 | 345 | @doc """ 346 | Implementation of `Enum.unzip/1` for more-than-two-element tuples. 347 | 348 | Size of returned tuple is equal to size of the shortest tuple in `tuples`. 349 | 350 | ## Examples: 351 | 352 | iex> #{inspect(__MODULE__)}.unzip([{1,2,3}, {4,5,6}, {7,8,9}, {10,11,12}]) 353 | {[1, 4, 7, 10], [2, 5, 8, 11], [3, 6, 9, 12]} 354 | iex> #{inspect(__MODULE__)}.unzip([{1,2,3}, {4,5}, {6,7,8,9}, {10,11,12}]) 355 | {[1, 4, 6, 10], [2, 5, 7, 11]} 356 | 357 | """ 358 | @spec unzip(tuples :: [tuple()]) :: tuple() 359 | def unzip([]), do: {} 360 | 361 | def unzip([h | _] = list) when is_tuple(h) do 362 | do_unzip( 363 | list |> Enum.reverse(), 364 | [] |> repeated(h |> tuple_size()) 365 | ) 366 | end 367 | 368 | defp do_unzip([], acc) do 369 | acc |> List.to_tuple() 370 | end 371 | 372 | defp do_unzip([h | t], acc) when is_tuple(h) do 373 | acc = h |> Tuple.to_list() |> Enum.zip(acc) |> Enum.map(fn {t, r} -> [t | r] end) 374 | do_unzip(t, acc) 375 | end 376 | 377 | @spec duplicates(Enum.t(), pos_integer) :: list() 378 | @doc """ 379 | Returns elements that occur at least `min_occurences` times in enumerable. 380 | 381 | Results are NOT ordered in any sensible way, neither is the order anyhow preserved, 382 | but it is deterministic. 383 | 384 | ## Examples 385 | 386 | iex> Bunch.Enum.duplicates([1,3,2,5,3,2,2]) 387 | [2, 3] 388 | iex> Bunch.Enum.duplicates([1,3,2,5,3,2,2], 3) 389 | [2] 390 | 391 | """ 392 | def duplicates(enum, min_occurences \\ 2) do 393 | enum 394 | |> Enum.reduce({%{}, []}, fn v, {existent, duplicates} -> 395 | {occurrences, existent} = existent |> Map.get_and_update(v, &{&1 || 1, (&1 || 1) + 1}) 396 | duplicates = if occurrences == min_occurences, do: [v | duplicates], else: duplicates 397 | {existent, duplicates} 398 | end) 399 | ~> ({_, duplicates} -> duplicates) 400 | end 401 | end 402 | -------------------------------------------------------------------------------- /lib/bunch/kv_enum.ex: -------------------------------------------------------------------------------- 1 | defmodule Bunch.KVEnum do 2 | @moduledoc """ 3 | A bunch of helper functions for manipulating key-value enums (including keyword 4 | enums). 5 | 6 | Key-value enums are represented as enums of 2-element tuples, where the first 7 | element of each tuple is a key, and the second is a value. 8 | """ 9 | 10 | @type t(_key, _value) :: Enumerable.t() 11 | 12 | @doc """ 13 | Returns all keys from the `enum`. 14 | 15 | Duplicated keys appear duplicated in the final enum of keys. 16 | 17 | ## Examples 18 | 19 | iex> #{inspect(__MODULE__)}.keys(a: 1, b: 2) 20 | [:a, :b] 21 | iex> #{inspect(__MODULE__)}.keys(a: 1, b: 2, a: 3) 22 | [:a, :b, :a] 23 | 24 | """ 25 | @spec keys(t(key, value)) :: [key] when key: any, value: any 26 | def keys(enum) do 27 | Enum.map(enum, &Bunch.key/1) 28 | end 29 | 30 | @doc """ 31 | Returns all values from the `enum`. 32 | 33 | Values from duplicated keys will be kept in the final enum of values. 34 | 35 | ## Examples 36 | 37 | iex> #{inspect(__MODULE__)}.values(a: 1, b: 2) 38 | [1, 2] 39 | iex> #{inspect(__MODULE__)}.values(a: 1, b: 2, a: 3) 40 | [1, 2, 3] 41 | 42 | """ 43 | @spec values(t(key, value)) :: [value] when key: any, value: any 44 | def values(enum) do 45 | Enum.map(enum, &Bunch.value/1) 46 | end 47 | 48 | @doc """ 49 | Maps keys of `enum` using function `f`. 50 | 51 | ## Example 52 | 53 | iex> #{inspect(__MODULE__)}.map_keys([{1, :a}, {2, :b}], & &1+1) 54 | [{2, :a}, {3, :b}] 55 | 56 | """ 57 | @spec map_keys(t(k1, v), (k1 -> k2)) :: t(k2, v) when k1: any, k2: any, v: any 58 | def map_keys(enum, f) do 59 | enum |> Enum.map(fn {key, value} -> {f.(key), value} end) 60 | end 61 | 62 | @doc """ 63 | Maps values of `enum` using function `f`. 64 | 65 | ## Example 66 | 67 | iex> #{inspect(__MODULE__)}.map_values([a: 1, b: 2], & &1+1) 68 | [a: 2, b: 3] 69 | 70 | """ 71 | @spec map_values(t(k, v1), (v1 -> v2)) :: t(k, v2) when k: any, v1: any, v2: any 72 | def map_values(enum, f) do 73 | enum |> Enum.map(fn {key, value} -> {key, f.(value)} end) 74 | end 75 | 76 | @doc """ 77 | Filters elements of `enum` by keys using function `f`. 78 | 79 | ## Example 80 | 81 | iex> #{inspect(__MODULE__)}.filter_by_keys([a: 1, b: 2, a: 3], & &1 == :a) 82 | [a: 1, a: 3] 83 | 84 | """ 85 | @spec filter_by_keys(t(k, v), (k -> as_boolean(term))) :: t(k, v) when k: any, v: any 86 | def filter_by_keys(enum, f) do 87 | enum |> Enum.filter(&apply_to_key(&1, f)) 88 | end 89 | 90 | @doc """ 91 | Filters elements of `enum` by values using function `f`. 92 | 93 | ## Example 94 | 95 | iex> #{inspect(__MODULE__)}.filter_by_values([a: 1, b: 2, a: 3], & &1 |> rem(2) == 0) 96 | [b: 2] 97 | 98 | """ 99 | @spec filter_by_values(t(k, v), (v -> as_boolean(term))) :: t(k, v) when k: any, v: any 100 | def filter_by_values(enum, f) do 101 | enum |> Enum.filter(&apply_to_value(&1, f)) 102 | end 103 | 104 | @doc """ 105 | Executes `f` for each key in `enum`. 106 | 107 | ## Example 108 | 109 | iex> #{inspect(__MODULE__)}.each_key([a: 1, b: 2, a: 3], & send(self(), &1)) 110 | iex> [:a, :b, :a] |> Enum.each(&receive do ^&1 -> :ok end) 111 | :ok 112 | 113 | """ 114 | @spec each_key(t(k, v), (k -> any | no_return)) :: :ok when k: any, v: any 115 | def each_key(enum, f) do 116 | enum |> Enum.each(&apply_to_key(&1, f)) 117 | end 118 | 119 | @doc """ 120 | Executes `f` for each value in `enum`. 121 | 122 | ## Example 123 | 124 | iex> #{inspect(__MODULE__)}.each_value([a: 1, b: 2, a: 3], & send(self(), &1)) 125 | iex> 1..3 |> Enum.each(&receive do ^&1 -> :ok end) 126 | :ok 127 | 128 | """ 129 | @spec each_value(t(k, v), (v -> any | no_return)) :: :ok when k: any, v: any 130 | def each_value(enum, f) do 131 | enum |> Enum.each(&apply_to_value(&1, f)) 132 | end 133 | 134 | @doc """ 135 | Returns `true` if `f` returns truthy value for any key from `enum`, otherwise `false`. 136 | 137 | ## Example 138 | 139 | iex> #{inspect(__MODULE__)}.any_key?([a: 1, b: 2, a: 3], & &1 == :b) 140 | true 141 | iex> #{inspect(__MODULE__)}.any_key?([a: 1, b: 3, a: 5], & &1 == :c) 142 | false 143 | 144 | """ 145 | @spec any_key?(t(k, v), (k -> as_boolean(term))) :: boolean when k: any, v: any 146 | def any_key?(enum, f \\ & &1) do 147 | enum |> Enum.any?(&apply_to_key(&1, f)) 148 | end 149 | 150 | @doc """ 151 | Returns `true` if `f` returns truthy value for any value from `enum`, otherwise `false`. 152 | 153 | ## Example 154 | 155 | iex> #{inspect(__MODULE__)}.any_value?([a: 1, b: 2, a: 3], & &1 |> rem(2) == 0) 156 | true 157 | iex> #{inspect(__MODULE__)}.any_value?([a: 1, b: 3, a: 5], & &1 |> rem(2) == 0) 158 | false 159 | 160 | """ 161 | @spec any_value?(t(k, v), (v -> as_boolean(term))) :: boolean when k: any, v: any 162 | def any_value?(enum, f \\ & &1) do 163 | enum |> Enum.any?(&apply_to_value(&1, f)) 164 | end 165 | 166 | defp apply_to_key({key, _value}, f), do: f.(key) 167 | defp apply_to_value({_key, value}, f), do: f.(value) 168 | end 169 | -------------------------------------------------------------------------------- /lib/bunch/kv_list.ex: -------------------------------------------------------------------------------- 1 | defmodule Bunch.KVList do 2 | @deprecated "Use `Bunch.KVEnum` instead" 3 | @moduledoc """ 4 | A bunch of helper functions for manipulating key-value lists (including keyword 5 | lists). 6 | 7 | Key-value lists are represented as lists of 2-element tuples, where the first 8 | element of each tuple is a key, and the second is a value. 9 | """ 10 | 11 | @type t(key, value) :: [{key, value}] 12 | 13 | @doc """ 14 | Maps keys of `list` using function `f`. 15 | 16 | ## Example 17 | 18 | iex> #{inspect(__MODULE__)}.map_keys([{1, :a}, {2, :b}], & &1+1) 19 | [{2, :a}, {3, :b}] 20 | 21 | """ 22 | @spec map_keys(t(k1, v), (k1 -> k2)) :: t(k2, v) when k1: any, k2: any, v: any 23 | def map_keys(list, f) do 24 | list |> Enum.map(fn {key, value} -> {f.(key), value} end) 25 | end 26 | 27 | @doc """ 28 | Maps values of `list` using function `f`. 29 | 30 | ## Example 31 | 32 | iex> #{inspect(__MODULE__)}.map_values([a: 1, b: 2], & &1+1) 33 | [a: 2, b: 3] 34 | 35 | """ 36 | @spec map_values(t(k, v1), (v1 -> v2)) :: t(k, v2) when k: any, v1: any, v2: any 37 | def map_values(list, f) do 38 | list |> Enum.map(fn {key, value} -> {key, f.(value)} end) 39 | end 40 | 41 | @doc """ 42 | Filters elements of `list` by keys using function `f`. 43 | 44 | ## Example 45 | 46 | iex> #{inspect(__MODULE__)}.filter_by_keys([a: 1, b: 2, a: 3], & &1 == :a) 47 | [a: 1, a: 3] 48 | 49 | """ 50 | @spec filter_by_keys(t(k, v), (k -> as_boolean(term))) :: t(k, v) when k: any, v: any 51 | def filter_by_keys(list, f) do 52 | list |> Enum.filter(&apply_to_key(&1, f)) 53 | end 54 | 55 | @doc """ 56 | Filters elements of `list` by values using function `f`. 57 | 58 | ## Example 59 | 60 | iex> #{inspect(__MODULE__)}.filter_by_values([a: 1, b: 2, a: 3], & &1 |> rem(2) == 0) 61 | [b: 2] 62 | 63 | """ 64 | @spec filter_by_values(t(k, v), (v -> as_boolean(term))) :: t(k, v) when k: any, v: any 65 | def filter_by_values(list, f) do 66 | list |> Enum.filter(&apply_to_value(&1, f)) 67 | end 68 | 69 | @doc """ 70 | Executes `f` for each key in `list`. 71 | 72 | ## Example 73 | 74 | iex> #{inspect(__MODULE__)}.each_key([a: 1, b: 2, a: 3], & send(self(), &1)) 75 | iex> [:a, :b, :a] |> Enum.each(&receive do ^&1 -> :ok end) 76 | :ok 77 | 78 | """ 79 | @spec each_key(t(k, v), (k -> any | no_return)) :: :ok when k: any, v: any 80 | def each_key(list, f) do 81 | list |> Enum.each(&apply_to_key(&1, f)) 82 | end 83 | 84 | @doc """ 85 | Executes `f` for each value in `list`. 86 | 87 | ## Example 88 | 89 | iex> #{inspect(__MODULE__)}.each_value([a: 1, b: 2, a: 3], & send(self(), &1)) 90 | iex> 1..3 |> Enum.each(&receive do ^&1 -> :ok end) 91 | :ok 92 | 93 | """ 94 | @spec each_value(t(k, v), (v -> any | no_return)) :: :ok when k: any, v: any 95 | def each_value(list, f) do 96 | list |> Enum.each(&apply_to_value(&1, f)) 97 | end 98 | 99 | @doc """ 100 | Returns `true` if `f` returns truthy value for any key from `list`, otherwise `false`. 101 | 102 | ## Example 103 | 104 | iex> #{inspect(__MODULE__)}.any_key?([a: 1, b: 2, a: 3], & &1 == :b) 105 | true 106 | iex> #{inspect(__MODULE__)}.any_key?([a: 1, b: 3, a: 5], & &1 == :c) 107 | false 108 | 109 | """ 110 | @spec any_key?(t(k, v), (k -> as_boolean(term))) :: boolean when k: any, v: any 111 | def any_key?(list, f) do 112 | list |> Enum.any?(&apply_to_key(&1, f)) 113 | end 114 | 115 | @doc """ 116 | Returns `true` if `f` returns truthy value for any value from `list`, otherwise `false`. 117 | 118 | ## Example 119 | 120 | iex> #{inspect(__MODULE__)}.any_value?([a: 1, b: 2, a: 3], & &1 |> rem(2) == 0) 121 | true 122 | iex> #{inspect(__MODULE__)}.any_value?([a: 1, b: 3, a: 5], & &1 |> rem(2) == 0) 123 | false 124 | 125 | """ 126 | @spec any_value?(t(k, v), (v -> as_boolean(term))) :: boolean when k: any, v: any 127 | def any_value?(list, f) do 128 | list |> Enum.any?(&apply_to_value(&1, f)) 129 | end 130 | 131 | defp apply_to_key({key, _value}, f), do: f.(key) 132 | defp apply_to_value({_key, value}, f), do: f.(value) 133 | end 134 | -------------------------------------------------------------------------------- /lib/bunch/list.ex: -------------------------------------------------------------------------------- 1 | defmodule Bunch.List do 2 | @moduledoc """ 3 | A bunch of helper functions for list manipulation. 4 | """ 5 | 6 | @doc """ 7 | Generates a list using generator function and provided initial accumulator. 8 | 9 | Successive list elements are generated by calling `f` with the previous accumulator. 10 | The enumeration finishes when it returns `:halt` or `{:halt, accumulator}`. 11 | 12 | ## Examples 13 | 14 | iex> #{inspect(__MODULE__)}.unfoldr(10, fn 0 -> :halt; n -> {:cont, n-1} end) 15 | Enum.to_list(9..0) 16 | iex> f = fn 17 | ...> <> -> {:cont, content, rest} 18 | ...> binary -> {:halt, binary} 19 | ...> end 20 | iex> #{inspect(__MODULE__)}.unfoldr(<<2, "ab", 3, "cde", 4, "fghi">>, f) 21 | {~w(ab cde fghi), <<>>} 22 | iex> #{inspect(__MODULE__)}.unfoldr(<<2, "ab", 3, "cde", 4, "fg">>, f) 23 | {~w(ab cde), <<4, "fg">>} 24 | 25 | """ 26 | @spec unfoldr(acc, (acc -> {:cont, a, acc} | {:cont, acc} | :halt | {:halt, acc})) :: 27 | [a] | {[a], acc} 28 | when acc: any, a: any 29 | def unfoldr(acc, f) do 30 | do_unfoldr(acc, f, []) 31 | end 32 | 33 | defp do_unfoldr(acc, f, res_acc) do 34 | case f.(acc) do 35 | {:cont, value, acc} -> do_unfoldr(acc, f, [value | res_acc]) 36 | {:cont, acc} -> do_unfoldr(acc, f, [acc | res_acc]) 37 | {:halt, acc} -> {Enum.reverse(res_acc), acc} 38 | :halt -> Enum.reverse(res_acc) 39 | end 40 | end 41 | 42 | @doc """ 43 | The version of `unfoldr/2` that res_accepts `:ok` and `:error` return tuples. 44 | 45 | Behaves as `unfoldr/2` as long as `:ok` tuples are returned. Upon `:error` 46 | the processing is stopped and error is returned. 47 | 48 | ## Examples 49 | 50 | iex> f = fn 51 | iex> <> -> 52 | iex> sum = a + b 53 | iex> if rem(sum, 2) == 1, do: {:ok, {:cont, sum, rest}}, else: {:error, :even_sum} 54 | iex> acc -> {:ok, {:halt, acc}} 55 | iex> end 56 | iex> #{inspect(__MODULE__)}.try_unfoldr(<<1,2,3,4,5>>, f) 57 | {:ok, {[3, 7], <<5>>}} 58 | iex> #{inspect(__MODULE__)}.try_unfoldr(<<2,4,6,8>>, f) 59 | {:error, :even_sum} 60 | 61 | """ 62 | @spec try_unfoldr( 63 | acc, 64 | (acc -> 65 | {:ok, {:cont, a, acc} | {:cont, acc} | :halt | {:halt, acc}} | {:error, reason}) 66 | ) :: 67 | {:ok, [a] | {[a], acc}} | {:error, reason} 68 | when acc: any, a: any, reason: any 69 | def try_unfoldr(acc, f) do 70 | do_try_unfoldr(acc, f, []) 71 | end 72 | 73 | defp do_try_unfoldr(acc, f, res_acc) do 74 | case f.(acc) do 75 | {:ok, {:cont, value, acc}} -> do_try_unfoldr(acc, f, [value | res_acc]) 76 | {:ok, {:cont, acc}} -> do_try_unfoldr(acc, f, [acc | res_acc]) 77 | {:ok, {:halt, acc}} -> {:ok, {Enum.reverse(res_acc), acc}} 78 | {:ok, :halt} -> {:ok, Enum.reverse(res_acc)} 79 | {:error, reason} -> {:error, reason} 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/bunch/macro.ex: -------------------------------------------------------------------------------- 1 | defmodule Bunch.Macro do 2 | @moduledoc """ 3 | A bunch of helpers for implementing macros. 4 | """ 5 | 6 | @doc """ 7 | Imitates `import` functionality by finding and replacing bare function 8 | calls (like `foo()`) in AST with fully-qualified call (like `Some.Module.foo()`) 9 | 10 | Receives AST fragment as first parameter and 11 | list of pairs {Some.Module, :foo} as second 12 | """ 13 | @spec inject_calls(Macro.t(), [{module(), atom()}]) :: Macro.t() 14 | def inject_calls(ast, functions) 15 | when is_list(functions) do 16 | Macro.prewalk(ast, fn ast_node -> 17 | functions |> Enum.reduce(ast_node, &replace_call(&2, &1)) 18 | end) 19 | end 20 | 21 | @doc """ 22 | Imitates `import` functionality by finding and replacing bare function 23 | calls (like `foo()`) in AST with fully-qualified call (like `Some.Module.foo()`) 24 | 25 | Receives AST fragment as first parameter and 26 | a pair {Some.Module, :foo} as second 27 | """ 28 | @spec inject_call(Macro.t(), {module(), atom()}) :: Macro.t() 29 | def inject_call(ast, {module, fun_name}) 30 | when is_atom(module) and is_atom(fun_name) do 31 | Macro.prewalk(ast, fn ast_node -> 32 | replace_call(ast_node, {module, fun_name}) 33 | end) 34 | end 35 | 36 | defp replace_call(ast_node, {module, fun_name}) 37 | when is_atom(module) and is_atom(fun_name) do 38 | case ast_node do 39 | {^fun_name, _ctx, args} -> 40 | quote do 41 | apply(unquote(module), unquote(fun_name), unquote(args)) 42 | end 43 | 44 | other_node -> 45 | other_node 46 | end 47 | end 48 | 49 | @doc """ 50 | Works like `Macro.prewalk/2`, but allows to skip particular nodes. 51 | 52 | ## Example 53 | 54 | iex> code = quote do fun(1, 2, opts: [key: :val]) end 55 | iex> code |> Bunch.Macro.prewalk_while(fn node -> 56 | ...> if Keyword.keyword?(node) do 57 | ...> {:skip, node ++ [default: 1]} 58 | ...> else 59 | ...> {:enter, node} 60 | ...> end 61 | ...> end) 62 | quote do fun(1, 2, opts: [key: :val], default: 1) end 63 | 64 | """ 65 | @spec prewalk_while(Macro.t(), (Macro.t() -> {:enter | :skip, Macro.t()})) :: Macro.t() 66 | def prewalk_while(ast, fun) do 67 | {ast, :not_skipping} = 68 | Macro.traverse( 69 | ast, 70 | :not_skipping, 71 | fn node, :not_skipping -> 72 | case fun.(node) do 73 | {:enter, node} -> {node, :not_skipping} 74 | {:skip, node} -> {nil, {:skipping, node}} 75 | end 76 | end, 77 | fn 78 | nil, {:skipping, node} -> {node, :not_skipping} 79 | node, :not_skipping -> {node, :not_skipping} 80 | end 81 | ) 82 | 83 | ast 84 | end 85 | 86 | @doc """ 87 | Works like `Macro.prewalk/3`, but allows to skip particular nodes using an accumulator. 88 | 89 | ## Example 90 | 91 | iex> code = quote do fun(1, 2, opts: [key: :val]) end 92 | iex> code |> Bunch.Macro.prewalk_while(0, fn node, acc -> 93 | ...> if Keyword.keyword?(node) do 94 | ...> {:skip, node ++ [default: 1], acc + 1} 95 | ...> else 96 | ...> {:enter, node, acc} 97 | ...> end 98 | ...> end) 99 | {quote do fun(1, 2, opts: [key: :val], default: 1) end, 1} 100 | 101 | """ 102 | @spec prewalk_while( 103 | Macro.t(), 104 | any(), 105 | (Macro.t(), any() -> {:enter | :skip, Macro.t(), any()}) 106 | ) :: {Macro.t(), any()} 107 | def prewalk_while(ast, acc, fun) do 108 | {ast, {acc, :not_skipping}} = 109 | Macro.traverse( 110 | ast, 111 | {acc, :not_skipping}, 112 | fn node, {acc, :not_skipping} -> 113 | case fun.(node, acc) do 114 | {:enter, node, acc} -> {node, {acc, :not_skipping}} 115 | {:skip, node, acc} -> {nil, {acc, {:skipping, node}}} 116 | end 117 | end, 118 | fn 119 | nil, {acc, {:skipping, node}} -> {node, {acc, :not_skipping}} 120 | node, {acc, :not_skipping} -> {node, {acc, :not_skipping}} 121 | end 122 | ) 123 | 124 | {ast, acc} 125 | end 126 | 127 | @doc """ 128 | Receives an AST and traverses it expanding all the nodes. 129 | 130 | This function uses `Macro.expand/2` under the hood. Check 131 | it out for more information and examples. 132 | """ 133 | @spec expand_deep(Macro.t(), Macro.Env.t()) :: Macro.t() 134 | def expand_deep(ast, env), do: Macro.prewalk(ast, fn tree -> Macro.expand(tree, env) end) 135 | end 136 | -------------------------------------------------------------------------------- /lib/bunch/map.ex: -------------------------------------------------------------------------------- 1 | defmodule Bunch.Map do 2 | @moduledoc """ 3 | A bunch of helper functions for manipulating maps. 4 | """ 5 | use Bunch 6 | 7 | @doc """ 8 | Updates value at `key` in `map` and returns new value and updated map. 9 | 10 | Uses `Map.get_and_update/3` under the hood. 11 | 12 | ## Example 13 | 14 | iex> %{a: 1} |> #{inspect(__MODULE__)}.get_updated(:a, & &1+1) 15 | {2, %{a: 2}} 16 | 17 | """ 18 | @spec get_updated(map, Map.key(), (Map.value() -> v)) :: {v, map} when v: Map.value() 19 | def get_updated(map, key, fun) do 20 | Map.get_and_update(map, key, fn a -> fun.(a) ~> {&1, &1} end) 21 | end 22 | 23 | @doc """ 24 | Works like `get_updated/3`, but requires `map` to contain `key`. 25 | 26 | Uses `Map.get_and_update!/3` under the hood. 27 | 28 | ## Example 29 | 30 | iex> %{a: 1} |> #{inspect(__MODULE__)}.get_updated!(:a, & &1+1) 31 | {2, %{a: 2}} 32 | 33 | """ 34 | @spec get_updated!(map, Map.key(), (Map.value() -> v)) :: {v, map} when v: Map.value() 35 | def get_updated!(map, key, fun) do 36 | Map.get_and_update!(map, key, fn a -> fun.(a) ~> {&1, &1} end) 37 | end 38 | 39 | @doc """ 40 | Maps keys of `map` using function `f`. 41 | 42 | ## Example 43 | 44 | iex> #{inspect(__MODULE__)}.map_keys(%{1 => :a, 2 => :b}, & &1+1) 45 | %{2 => :a, 3 => :b} 46 | 47 | """ 48 | @spec map_keys(%{k1 => v}, (k1 -> k2)) :: %{k2 => v} when k1: any, k2: any, v: any 49 | def map_keys(map, f) do 50 | map |> Enum.into(Map.new(), fn {key, value} -> {f.(key), value} end) 51 | end 52 | 53 | @doc """ 54 | Maps values of `map` using function `f`. 55 | 56 | ## Example 57 | 58 | iex> #{inspect(__MODULE__)}.map_values(%{a: 1, b: 2}, & &1+1) 59 | %{a: 2, b: 3} 60 | 61 | """ 62 | @spec map_values(%{k => v1}, (v1 -> v2)) :: %{k => v2} when k: any, v1: any, v2: any 63 | def map_values(map, f) do 64 | map |> Enum.into(Map.new(), fn {key, value} -> {key, f.(value)} end) 65 | end 66 | 67 | @doc """ 68 | Moves value stored at `old_key` to `new_key`. 69 | 70 | If `old_key` is not present in `map`, `default_value` is stored at `new_key`. 71 | If `new_key` is present in `map`, it's value is overwritten. 72 | 73 | ## Examples 74 | 75 | iex> #{inspect(__MODULE__)}.move(%{a: 1, b: 2}, :a, :c, 3) 76 | %{b: 2, c: 1} 77 | iex> #{inspect(__MODULE__)}.move(%{a: 1, b: 2}, :a, :b, 3) 78 | %{b: 1} 79 | iex> #{inspect(__MODULE__)}.move(%{a: 1, b: 2}, :c, :b, 3) 80 | %{a: 1, b: 3} 81 | 82 | """ 83 | @spec move(%{k => v}, old_key :: k, new_key :: k, default_value :: v) :: %{k => v} 84 | when k: any, v: any 85 | def move(map, old_key, new_key, default_value) do 86 | {value, map} = map |> Map.pop(old_key, default_value) 87 | map |> Map.put(new_key, value) 88 | end 89 | 90 | @doc """ 91 | Works like `move/3`, but fails if either `old_key` is absent or `new_key` is present 92 | in `map`. 93 | 94 | ## Example 95 | 96 | iex> #{inspect(__MODULE__)}.move!(%{a: 1, b: 2}, :a, :c) 97 | %{b: 2, c: 1} 98 | 99 | """ 100 | @spec move!(%{k => v}, old_key :: k, new_key :: k) :: %{k => v} | no_return 101 | when k: any, v: any 102 | def move!(map, old_key, new_key) do 103 | true = Map.has_key?(map, old_key) and not Map.has_key?(map, new_key) 104 | {value, map} = map |> Map.pop(old_key) 105 | map |> Map.put(new_key, value) 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/bunch/markdown.ex: -------------------------------------------------------------------------------- 1 | defmodule Bunch.Markdown do 2 | @moduledoc """ 3 | A bunch of helpers for generating Markdown text 4 | """ 5 | 6 | @doc """ 7 | Indents whole block of text by specified number of spaces 8 | 9 | ## Examples 10 | 11 | iex>#{inspect(__MODULE__)}.indent("text") 12 | " text" 13 | 14 | iex>text = \""" 15 | ...>First line 16 | ...>Second line 17 | ...>Third line 18 | ...>\""" 19 | iex>#{inspect(__MODULE__)}.indent(text) 20 | \""" 21 | First line 22 | Second line 23 | Third line 24 | \""" 25 | iex>#{inspect(__MODULE__)}.indent(text, 4) 26 | \""" 27 | First line 28 | Second line 29 | Third line 30 | \""" 31 | """ 32 | 33 | @spec indent(String.t(), non_neg_integer()) :: String.t() 34 | def indent(string, level \\ 2) do 35 | do_indent(string, level) 36 | end 37 | 38 | @doc """ 39 | Indents the whole block of text by specified number of hard spaces (` `). 40 | 41 | ## Examples 42 | 43 | iex>#{inspect(__MODULE__)}.hard_indent("text") 44 | "  text" 45 | 46 | iex>text = \""" 47 | ...>First line 48 | ...>Second line 49 | ...>Third line 50 | ...>\""" 51 | iex>#{inspect(__MODULE__)}.hard_indent(text) 52 | \""" 53 |   First line 54 |   Second line 55 |   Third line 56 | \""" 57 | iex>#{inspect(__MODULE__)}.hard_indent(text, 1) 58 | \""" 59 |  First line 60 |  Second line 61 |  Third line 62 | \""" 63 | """ 64 | @spec hard_indent(String.t(), non_neg_integer()) :: String.t() 65 | def hard_indent(string, level \\ 2) do 66 | do_indent(string, level, " ") 67 | end 68 | 69 | defp do_indent(string, size, character \\ " ") do 70 | indent = String.duplicate(character, size) 71 | 72 | string 73 | |> String.replace("\n", "\n" <> indent) 74 | |> String.replace_suffix(indent, "") 75 | |> String.replace_prefix("", indent) 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/bunch/math.ex: -------------------------------------------------------------------------------- 1 | defmodule Bunch.Math do 2 | @moduledoc """ 3 | A bunch of math helper functions. 4 | """ 5 | 6 | @doc """ 7 | Applies `div/2` and `rem/2` to arguments and returns results as a tuple. 8 | 9 | ## Example 10 | 11 | iex> #{inspect(__MODULE__)}.div_rem(10, 4) 12 | {div(10, 4), rem(10, 4)} 13 | 14 | """ 15 | @spec div_rem(divident :: non_neg_integer, divisor :: pos_integer) :: 16 | {div :: non_neg_integer, rem :: non_neg_integer} 17 | def div_rem(dividend, divisor) do 18 | {div(dividend, divisor), rem(dividend, divisor)} 19 | end 20 | 21 | @doc """ 22 | Works like `div_rem/2` but allows to accumulate remainder. 23 | 24 | Useful when an accumulation of division error is not acceptable, for example 25 | when you need to produce chunks of data every second but need to make sure there 26 | are 9 chunks per 4 seconds on average. You can calculate `div_rem(9, 4)`, 27 | keep the remainder, pass it to subsequent calls and every fourth result will be 28 | bigger than others. 29 | 30 | ## Example 31 | 32 | iex> 1..10 |> Enum.map_reduce(0, fn _, err -> 33 | ...> #{inspect(__MODULE__)}.div_rem(9, 4, err) 34 | ...> end) 35 | {[2, 2, 2, 3, 2, 2, 2, 3, 2, 2], 2} 36 | 37 | """ 38 | @spec div_rem( 39 | divident :: non_neg_integer, 40 | divisor :: pos_integer, 41 | accumulated_remainder :: non_neg_integer 42 | ) :: {div :: non_neg_integer, rem :: non_neg_integer} 43 | def div_rem(dividend, divisor, accumulated_remainder) do 44 | div_rem(accumulated_remainder + dividend, divisor) 45 | end 46 | 47 | @doc """ 48 | Returns the biggest multiple of `value` that is lower than or equal to `threshold`. 49 | 50 | ## Examples 51 | 52 | iex> #{inspect(__MODULE__)}.max_multiple_lte(4, 10) 53 | 8 54 | iex> #{inspect(__MODULE__)}.max_multiple_lte(2, 6) 55 | 6 56 | 57 | """ 58 | @spec max_multiple_lte(value :: pos_integer, threshold :: non_neg_integer) :: non_neg_integer 59 | def max_multiple_lte(value, threshold) do 60 | remainder = threshold |> rem(value) 61 | threshold - remainder 62 | end 63 | 64 | @doc """ 65 | Returns the smallest multiple of `value` that is greater than or equal to `threshold`. 66 | 67 | ## Examples 68 | 69 | iex> #{inspect(__MODULE__)}.min_multiple_gte(4, 10) 70 | 12 71 | iex> #{inspect(__MODULE__)}.min_multiple_gte(2, 6) 72 | 6 73 | 74 | """ 75 | @spec min_multiple_gte(value :: pos_integer, threshold :: non_neg_integer) :: non_neg_integer 76 | def min_multiple_gte(value, threshold) do 77 | case threshold |> rem(value) do 78 | 0 -> threshold 79 | remainder -> threshold + value - remainder 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/bunch/module.ex: -------------------------------------------------------------------------------- 1 | defmodule Bunch.Module do 2 | @moduledoc """ 3 | A bunch of functions for easier manipulation on modules. 4 | """ 5 | 6 | @doc """ 7 | Determines whether module implements a behaviour by checking a test function. 8 | 9 | Checked behaviour needs to define a callback with unique name and no arguments, 10 | that should return `true`. This functions ensures that the module is loaded and 11 | checks if it exports implementation of the callback that returns `true`. If 12 | all these conditions are met, `true` is returned. Otherwise returns `false`. 13 | """ 14 | @spec check_behaviour(module, atom) :: boolean 15 | def check_behaviour(module, fun_name) do 16 | module |> loaded_and_function_exported?(fun_name, 0) and module |> apply(fun_name, []) 17 | end 18 | 19 | @doc """ 20 | Returns instance of struct defined in given module, if the module defines struct. 21 | Otherwise returns `nil`. 22 | 23 | Raises if struct has any required fields. 24 | """ 25 | @spec struct(module) :: struct | nil 26 | def struct(module) do 27 | if module |> loaded_and_function_exported?(:__struct__, 0), 28 | do: module.__struct__([]), 29 | else: nil 30 | end 31 | 32 | @doc """ 33 | Ensures that module is loaded and checks whether it exports given function. 34 | """ 35 | @spec loaded_and_function_exported?(module, atom, non_neg_integer) :: boolean 36 | def loaded_and_function_exported?(module, fun_name, arity) do 37 | module |> Code.ensure_loaded?() and module |> function_exported?(fun_name, arity) 38 | end 39 | 40 | @doc """ 41 | Works like `Kernel.apply/3` if `module` exports `fun_name/length(args)`, 42 | otherwise returns `default`. 43 | 44 | Determines if function is exported using `loaded_and_function_exported?/3`. 45 | """ 46 | @spec apply(module, fun_name :: atom, args :: list, default :: any) :: any 47 | def apply(module, fun_name, args, default) do 48 | if module |> loaded_and_function_exported?(fun_name, length(args)) do 49 | module |> Kernel.apply(fun_name, args) 50 | else 51 | default 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/bunch/retry.ex: -------------------------------------------------------------------------------- 1 | defmodule Bunch.Retry do 2 | @moduledoc """ 3 | A bunch of helpers for handling scenarios when some actions should be repeated 4 | until it succeeds. 5 | """ 6 | 7 | @typedoc """ 8 | Possible options for `retry/3`. 9 | """ 10 | @type retry_option_t :: 11 | {:times, non_neg_integer()} 12 | | {:duration, milliseconds :: pos_integer} 13 | | {:delay, milliseconds :: pos_integer} 14 | 15 | @doc """ 16 | Calls `fun` function until `arbiter` function decides to stop. 17 | 18 | Possible options are: 19 | - times - limits amount of retries (first evaluation is not considered a retry) 20 | - duration - limits total time of execution of this function, but breaks only 21 | before subsequent retry 22 | - delay - introduces delay (`:timer.sleep/1`) before each retry 23 | 24 | ## Examples 25 | 26 | iex> {:ok, pid} = Agent.start_link(fn -> 0 end) 27 | iex> #{inspect(__MODULE__)}.retry(fn -> Agent.get_and_update(pid, &{&1, &1+1}) end, & &1 > 3) 28 | 4 29 | iex> {:ok, pid} = Agent.start_link(fn -> 0 end) 30 | iex> #{inspect(__MODULE__)}.retry(fn -> Agent.get_and_update(pid, &{&1, &1+1}) end, & &1 > 3, times: 10) 31 | 4 32 | iex> {:ok, pid} = Agent.start_link(fn -> 0 end) 33 | iex> #{inspect(__MODULE__)}.retry(fn -> Agent.get_and_update(pid, &{&1, &1+1}) end, & &1 > 3, times: 2) 34 | 2 35 | iex> {:ok, pid} = Agent.start_link(fn -> 0 end) 36 | iex> #{inspect(__MODULE__)}.retry( 37 | ...> fn -> :timer.sleep(100); Agent.get_and_update(pid, &{&1, &1+1}) end, 38 | ...> & &1 > 3, 39 | ...> duration: 150 40 | ...> ) 41 | 1 42 | iex> {:ok, pid} = Agent.start_link(fn -> 0 end) 43 | iex> #{inspect(__MODULE__)}.retry( 44 | ...> fn -> :timer.sleep(30); Agent.get_and_update(pid, &{&1, &1+1}) end, 45 | ...> & &1 > 3, 46 | ...> duration: 80, delay: 20 47 | ...> ) 48 | 1 49 | 50 | """ 51 | @spec retry( 52 | fun :: (-> res), 53 | arbiter :: (res -> stop? :: boolean), 54 | options :: [retry_option_t()] 55 | ) :: res 56 | when res: any() 57 | def retry(fun, arbiter, options \\ []) do 58 | times = options |> Keyword.get(:times, :infinity) 59 | duration = options |> Keyword.get(:duration, :infinity) 60 | delay = options |> Keyword.get(:delay, 0) 61 | fun |> do_retry(arbiter, times, duration, delay, 0, System.monotonic_time(:millisecond)) 62 | end 63 | 64 | defp do_retry(fun, arbiter, times, duration, delay, retries, init_time) do 65 | ret = fun.() 66 | 67 | if not arbiter.(ret) and times > retries && 68 | duration > System.monotonic_time(:millisecond) - init_time + delay do 69 | :timer.sleep(delay) 70 | fun |> do_retry(arbiter, times, duration, delay, retries + 1, init_time) 71 | else 72 | ret 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/bunch/short_ref.ex: -------------------------------------------------------------------------------- 1 | defmodule Bunch.ShortRef do 2 | @moduledoc """ 3 | A wrapper over Erlang/Elixir references that makes them more readable and visually 4 | distinguishable. 5 | 6 | 7 | ## Erlang references 8 | When printed, Erlang references are quite long: `#Reference<0.133031758.722993155.68472>`. 9 | Moreover, since they're based on an incremented counter, it's hard to distinguish 10 | between a two created within the same period of time, like `#Reference<0.133031758.722993155.68512>` 11 | and `#Reference<0.133031758.722993155.68519>`. 12 | 13 | ## #{inspect(__MODULE__)} 14 | `t:#{inspect(__MODULE__)}.t/0` stores a usual reference along with first 4 bytes 15 | of its SHA1 hash and when inspected prints only 8 hex digits prepended with `#`. 16 | Thanks to use of the hash function, similar references have totally different 17 | string representations - the case from the previous example would be `#60e0fd2d` 18 | and `#d4208051`. 19 | 20 | ## When to use 21 | `#{inspect(__MODULE__)}` should be used when a reference is to be printed or logged. 22 | It should NOT be used when creating lots of references as it adds a significant 23 | performance overhead. 24 | """ 25 | @enforce_keys [:ref, :hash] 26 | defstruct @enforce_keys 27 | 28 | @type t :: %__MODULE__{ref: reference, hash: String.t()} 29 | 30 | @doc """ 31 | Creates a short reference. 32 | 33 | iex> IEx.Helpers.ref(0, 1, 2, 3) |> #{inspect(__MODULE__)}.new() |> inspect() 34 | "#82c033ef" 35 | iex> <<"#", hash::binary-size(8)>> = #{inspect(__MODULE__)}.new() |> inspect() 36 | iex> Base.decode16(hash, case: :lower) |> elem(0) 37 | :ok 38 | 39 | """ 40 | @spec new(reference) :: t 41 | def new(ref \\ make_ref()) do 42 | ref_list = :erlang.ref_to_list(ref) 43 | <> = :crypto.hash(:sha, ref_list) 44 | hash = "#" <> Base.encode16(bin_hash_part, case: :lower) 45 | %__MODULE__{ref: ref, hash: hash} 46 | end 47 | end 48 | 49 | defimpl Inspect, for: Bunch.ShortRef do 50 | @impl true 51 | def inspect(%Bunch.ShortRef{hash: hash}, _opts), do: hash 52 | end 53 | -------------------------------------------------------------------------------- /lib/bunch/struct.ex: -------------------------------------------------------------------------------- 1 | defmodule Bunch.Struct do 2 | @moduledoc """ 3 | A bunch of functions for easier manipulation on structs. 4 | """ 5 | 6 | use Bunch 7 | 8 | import Kernel, except: [get_in: 2, put_in: 2, update_in: 3, get_and_update_in: 3, pop_in: 2] 9 | 10 | @compile {:inline, map_keys: 1} 11 | 12 | @gen_common_docs fn fun_name -> 13 | """ 14 | Wraps `Bunch.Access.#{fun_name}` to make it work with structs that do not 15 | implement `Access` behaviour. 16 | """ 17 | end 18 | 19 | @doc """ 20 | #{@gen_common_docs.("get_in/2")} 21 | """ 22 | @spec get_in(struct, Access.key() | [Access.key()]) :: Access.value() 23 | def get_in(struct, keys), do: struct |> Bunch.Access.get_in(keys |> map_keys()) 24 | 25 | @doc """ 26 | #{@gen_common_docs.("put_in/3")} 27 | """ 28 | @spec put_in(struct, Access.key() | [Access.key()], Access.value()) :: Access.value() 29 | def put_in(struct, keys, v), do: struct |> Bunch.Access.put_in(keys |> map_keys(), v) 30 | 31 | @doc """ 32 | #{@gen_common_docs.("update_in/3")} 33 | """ 34 | @spec update_in(struct, Access.key() | [Access.key()], (Access.value() -> Access.value())) :: 35 | struct 36 | def update_in(struct, keys, f), do: struct |> Bunch.Access.update_in(keys |> map_keys(), f) 37 | 38 | @doc """ 39 | #{@gen_common_docs.("get_and_update_in/3")} 40 | """ 41 | @spec get_and_update_in(struct, Access.key() | [Access.key()], (a -> {b, a})) :: {b, struct} 42 | when a: Access.value(), b: any 43 | def get_and_update_in(struct, keys, f), 44 | do: struct |> Bunch.Access.get_and_update_in(keys |> map_keys(), f) 45 | 46 | @doc """ 47 | #{@gen_common_docs.("pop_in/2")} 48 | """ 49 | @spec pop_in(struct, Access.key() | [Access.key()]) :: {Access.value(), struct} 50 | def pop_in(struct, keys), do: struct |> Bunch.Access.pop_in(keys |> map_keys()) 51 | 52 | @doc """ 53 | #{@gen_common_docs.("delete_in/2")} 54 | """ 55 | @spec delete_in(struct, Access.key() | [Access.key()]) :: struct 56 | def delete_in(struct, keys), do: struct |> Bunch.Access.delete_in(keys |> map_keys()) 57 | 58 | @spec map_keys(Access.key() | [Access.key()]) :: [Access.access_fun(struct | map, term)] 59 | defp map_keys(keys), do: keys |> Bunch.listify() |> Enum.map(&Access.key(&1, nil)) 60 | end 61 | -------------------------------------------------------------------------------- /lib/bunch/type.ex: -------------------------------------------------------------------------------- 1 | defmodule Bunch.Type do 2 | @moduledoc """ 3 | A bunch of commonly used types. 4 | """ 5 | 6 | @typedoc """ 7 | Represents result of an operation that may succeed or fail. 8 | """ 9 | @type try_t :: :ok | {:error, reason :: any} 10 | 11 | @typedoc """ 12 | Represents result of an operation that may return something or fail. 13 | """ 14 | @type try_t(value) :: {:ok, value} | {:error, reason :: any} 15 | 16 | @typedoc """ 17 | Represents a value along with state. 18 | """ 19 | @type stateful_t(value, state) :: {value, state} 20 | 21 | @typedoc """ 22 | Represents a `t:try_t/0` value along with state. 23 | """ 24 | @type stateful_try_t(state) :: stateful_t(try_t, state) 25 | 26 | @typedoc """ 27 | Represents a `t:try_t/1` value along with state. 28 | """ 29 | @type stateful_try_t(value, state) :: stateful_t(try_t(value), state) 30 | end 31 | -------------------------------------------------------------------------------- /lib/bunch/typespec.ex: -------------------------------------------------------------------------------- 1 | defmodule Bunch.Typespec do 2 | @moduledoc """ 3 | A bunch of typespec-related helpers. 4 | """ 5 | 6 | @deprecated "`use Bunch.Typespec` only brings to scope the deprecated @/1" 7 | defmacro __using__(_args) do 8 | quote do 9 | import Kernel, except: [@: 1] 10 | import unquote(__MODULE__), only: [@: 1] 11 | end 12 | end 13 | 14 | @doc """ 15 | **This macro is deprecated. Use `#{inspect(__MODULE__)}.enum_to_alternative/1` instead.** 16 | 17 | Allows to define a type in form of `t :: x | y | z | ...` and a module parameter 18 | in form of `@t [x, y, z, ...]` at once. 19 | 20 | ## Example 21 | 22 | defmodule Abc do 23 | use #{inspect(__MODULE__)} 24 | @list_type t :: [:a, :b, :c] 25 | @spec get_at(0..2) :: t 26 | def get_at(x), do: @t |> Enum.at(x) 27 | end 28 | 29 | Abc.get_at(1) # -> :b 30 | 31 | """ 32 | defmacro @{:list_type, _meta1, [{:"::", _meta2, [{name, _meta3, _env} = name_var, list]}]} do 33 | IO.warn( 34 | "Bunch.Typespec.@list_type is deprecated. Use #{inspect(__MODULE__)}.enum_to_alternative/1 instead." 35 | ) 36 | 37 | type = 38 | quote do 39 | Enum.reduce(unquote(list), fn a, b -> {:|, [], [a, b]} end) 40 | end 41 | 42 | type = {:unquote, [], [type]} 43 | 44 | quote do 45 | @type unquote(name_var) :: unquote(type) 46 | Module.put_attribute(__MODULE__, unquote(name), unquote(list)) 47 | end 48 | end 49 | 50 | defmacro @expr do 51 | quote do 52 | Kernel.@(unquote(expr)) 53 | end 54 | end 55 | 56 | @doc """ 57 | Converts an enumerable of terms to AST of alternative type of these terms. 58 | 59 | Useful for defining a type out of a list of constants. 60 | 61 | ## Examples 62 | 63 | iex> defmodule Example do 64 | ...> @values [1, :a, {true, 3 * 4}] 65 | ...> @type value :: unquote(#{inspect(__MODULE__)}.enum_to_alternative(@values)) 66 | ...> @spec get_value(0..2) :: value 67 | ...> def get_value(i), do: Enum.at(@values, i) 68 | ...> end 69 | iex> Example.get_value(1) 70 | :a 71 | 72 | iex> #{inspect(__MODULE__)}.enum_to_alternative([1, :a, {true, 3 * 4}]) 73 | quote do 74 | 1 | :a | {true, 12} 75 | end 76 | 77 | """ 78 | @spec enum_to_alternative(Enumerable.t()) :: Macro.t() 79 | def enum_to_alternative(list) do 80 | list |> Enum.reverse() |> Enum.reduce(fn a, b -> quote do: unquote(a) | unquote(b) end) 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Bunch.MixProject do 2 | use Mix.Project 3 | 4 | @version "1.6.1" 5 | @github_url "https://github.com/membraneframework/bunch" 6 | 7 | def project do 8 | [ 9 | app: :bunch, 10 | version: @version, 11 | elixir: "~> 1.12", 12 | start_permanent: Mix.env() == :prod, 13 | deps: deps(), 14 | dialyzer: dialyzer(), 15 | 16 | # hex 17 | description: "A bunch of helper functions, intended to make life easier", 18 | package: package(), 19 | 20 | # docs 21 | name: "Bunch", 22 | source_url: @github_url, 23 | homepage_url: "https://membraneframework.org", 24 | docs: docs() 25 | ] 26 | end 27 | 28 | def application do 29 | [extra_applications: [:crypto, :logger]] 30 | end 31 | 32 | defp docs do 33 | [ 34 | main: "readme", 35 | extras: ["README.md", "LICENSE"], 36 | formatters: ["html"], 37 | source_ref: "v#{@version}", 38 | nest_modules_by_prefix: [Bunch] 39 | ] 40 | end 41 | 42 | defp package do 43 | [ 44 | maintainers: ["Membrane Team"], 45 | licenses: ["Apache-2.0"], 46 | links: %{ 47 | "GitHub" => @github_url, 48 | "Membrane Framework Homepage" => "https://membraneframework.org" 49 | } 50 | ] 51 | end 52 | 53 | defp deps do 54 | [ 55 | {:ex_doc, "~> 0.28", only: :dev, runtime: false}, 56 | {:credo, "~> 1.6", only: :dev, runtime: false}, 57 | {:dialyxir, "~> 1.1", only: :dev, runtime: false} 58 | ] 59 | end 60 | 61 | defp dialyzer() do 62 | opts = [ 63 | flags: [:error_handling] 64 | ] 65 | 66 | if System.get_env("CI") == "true" do 67 | # Store PLTs in cacheable directory for CI 68 | [plt_local_path: "priv/plts", plt_core_path: "priv/plts"] ++ opts 69 | else 70 | opts 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, 3 | "credo": {:hex, :credo, "1.7.1", "6e26bbcc9e22eefbff7e43188e69924e78818e2fe6282487d0703652bc20fd62", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9871c6095a4c0381c89b6aa98bc6260a8ba6addccf7f6a53da8849c748a58a2"}, 4 | "dialyxir": {:hex, :dialyxir, "1.4.2", "764a6e8e7a354f0ba95d58418178d486065ead1f69ad89782817c296d0d746a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "516603d8067b2fd585319e4b13d3674ad4f314a5902ba8130cd97dc902ce6bbd"}, 5 | "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, 6 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 7 | "ex_doc": {:hex, :ex_doc, "0.30.9", "d691453495c47434c0f2052b08dd91cc32bc4e1a218f86884563448ee2502dd2", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "d7aaaf21e95dc5cddabf89063327e96867d00013963eadf2c6ad135506a8bc10"}, 8 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 9 | "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, 10 | "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, 11 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, 12 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, 13 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 14 | } 15 | -------------------------------------------------------------------------------- /test/bunch/access_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bunch.AccessTest do 2 | use ExUnit.Case, async: true 3 | 4 | @module Bunch.Access 5 | 6 | doctest @module 7 | end 8 | -------------------------------------------------------------------------------- /test/bunch/binary_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bunch.BinaryTest do 2 | use ExUnit.Case, async: true 3 | 4 | @module Bunch.Binary 5 | 6 | doctest @module 7 | end 8 | -------------------------------------------------------------------------------- /test/bunch/config_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bunch.ConfigTest do 2 | use ExUnit.Case, async: true 3 | 4 | @module Bunch.Config 5 | 6 | doctest @module 7 | 8 | test "config parsing" do 9 | assert {:error, {:config_field, {:key_not_found, :c}}} == 10 | @module.parse( 11 | [a: 1, b: 1], 12 | a: [validate: &(&1 > 0)], 13 | b: [in: -2..2], 14 | c: [] 15 | ) 16 | 17 | assert {:error, {:config_invalid_keys, [:c, :d]}} == 18 | @module.parse( 19 | [a: 1, b: 1, c: 2, d: 3], 20 | a: [validate: &(&1 > 0)], 21 | b: [in: -2..2], 22 | c: &if(&1.a != &1.b, do: []) 23 | ) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/bunch/enum_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bunch.EnumTest do 2 | use ExUnit.Case, async: true 3 | 4 | @module Bunch.Enum 5 | 6 | doctest @module 7 | end 8 | -------------------------------------------------------------------------------- /test/bunch/kv_enum_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bunch.KVEnumTest do 2 | use ExUnit.Case, async: true 3 | 4 | @module Bunch.KVEnum 5 | 6 | doctest @module 7 | end 8 | -------------------------------------------------------------------------------- /test/bunch/list_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bunch.ListTest do 2 | use ExUnit.Case, async: true 3 | 4 | @module Bunch.List 5 | 6 | doctest @module 7 | end 8 | -------------------------------------------------------------------------------- /test/bunch/macro_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bunch.MacroTest do 2 | use ExUnit.Case, async: true 3 | 4 | @module Bunch.Macro 5 | 6 | doctest @module 7 | end 8 | -------------------------------------------------------------------------------- /test/bunch/map_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bunch.MapTest do 2 | use ExUnit.Case, async: true 3 | 4 | @module Bunch.Map 5 | 6 | doctest @module 7 | end 8 | -------------------------------------------------------------------------------- /test/bunch/markdown_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bunch.MarkdownTest do 2 | use ExUnit.Case, async: true 3 | 4 | @module Bunch.Markdown 5 | 6 | doctest @module 7 | end 8 | -------------------------------------------------------------------------------- /test/bunch/math_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bunch.MathTest do 2 | use ExUnit.Case, async: true 3 | 4 | @module Bunch.Math 5 | 6 | doctest @module 7 | end 8 | -------------------------------------------------------------------------------- /test/bunch/retry_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bunch.RetryTest do 2 | use ExUnit.Case, async: true 3 | 4 | @module Bunch.Retry 5 | 6 | doctest @module 7 | end 8 | -------------------------------------------------------------------------------- /test/bunch/short_ref_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bunch.ShortRefTest do 2 | use ExUnit.Case, async: true 3 | 4 | @module Bunch.ShortRef 5 | 6 | doctest @module 7 | end 8 | -------------------------------------------------------------------------------- /test/bunch/typespec_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bunch.TypespecTest do 2 | use ExUnit.Case, async: true 3 | 4 | @module Bunch.Typespec 5 | 6 | doctest @module 7 | end 8 | -------------------------------------------------------------------------------- /test/bunch_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BunchTest do 2 | use Bunch 3 | use ExUnit.Case, async: true 4 | 5 | import ExUnit.CaptureIO 6 | 7 | @module Bunch 8 | 9 | doctest @module 10 | 11 | test "withl warns on redundant else labels" do 12 | warns = 13 | capture_io(:stderr, fn -> 14 | quote do 15 | withl a: _a <- 123, 16 | b: _a = 123 do 17 | :ok 18 | else 19 | a: _a -> :error 20 | b: _b -> :error 21 | c: _c -> :error 22 | end 23 | end 24 | |> Code.compile_quoted() 25 | end) 26 | 27 | refute String.contains?(warns, "withl's else clause labelled :a will never match") 28 | assert String.contains?(warns, "withl's else clause labelled :b will never match") 29 | assert String.contains?(warns, "withl's else clause labelled :c will never match") 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start(capture_log: true) 2 | --------------------------------------------------------------------------------