├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs.exs ├── docs.sh ├── elvis.config ├── rebar.config ├── rebar.lock ├── src ├── verl.app.src ├── verl.erl └── verl_parser.erl ├── test ├── prop_verl.erl ├── verl_SUITE.erl └── verl_parser_SUITE.erl └── vendor.sh /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: test 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [opened, synchronize] 9 | 10 | jobs: 11 | test: 12 | name: Erlang/OTP ${{matrix.otp}} / rebar3 ${{matrix.rebar3}} 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | otp: ['20', '21', '22', '23', '24'] 17 | rebar3: ['3.14.2', '3.14.3', '3.14.4', '3.15.0', '3.15.1'] 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: erlef/setup-beam@v1 21 | with: 22 | otp-version: ${{matrix.otp}} 23 | rebar3-version: ${{matrix.rebar3}} 24 | - name: Style review 25 | run: rebar3 lint 26 | - name: Cross reference analysis 27 | run: rebar3 as test xref 28 | - name: Static analysis 29 | run: rebar3 dialyzer 30 | - name: Common Tests 31 | run: rebar3 ct 32 | - name: Property tests 33 | run: rebar3 proper 34 | - name: Code coverage 35 | run: rebar3 as test do cover,covertool generate 36 | - name: Upload coverage report 37 | uses: codecov/codecov-action@v1 38 | with: 39 | files: _build/test/covertool/verl.covertool.xml 40 | name: ${{matrix.otp_vsn}} 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | .eunit 3 | *.o 4 | *.beam 5 | *.plt 6 | *.swp 7 | *.swo 8 | .erlang.cookie 9 | ebin 10 | log 11 | erl_crash.dump 12 | .rebar 13 | logs 14 | _build 15 | .idea 16 | *.iml 17 | rebar3.crashdump 18 | *~ 19 | -------------------------------------------------------------------------------- /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 | Copyright 2019, Bryan Paxton . 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # verl 2 | [![Hex Version](https://img.shields.io/hexpm/v/verl.svg)](https://hex.pm/packages/verl) [![GitHub Actions CI](https://github.com/jelly-beam/verl/workflows/build/badge.svg)](https://github.com/jelly-beam/verl 3 | ) [![codecov](https://codecov.io/gh/jelly-beam/verl/branch/main/graph/badge.svg)](https://codecov.io/gh/jelly-beam/verl) 4 | 5 | SemVer 2.0 version and requirements parsing, matching, and comparisons. 6 | 7 | All parsing of versions and requirements adhere to the [SemVer 2.0 schema](http://semver.org/) 8 | 9 | - [Build](#build) 10 | - [Usage](#usage) 11 | * [Comparisons](#comparisons) 12 | * [Version, Requirements, and Matching](#version--requirements--and-matching) 13 | - [Matching](#matching) 14 | - [Compiled requirements for ludicious speed matching](#compiled-requirements-for-ludicious-speed-matching) 15 | - [Version parsing](#version-parsing) 16 | * [Requirements parsing](#requirements-parsing) 17 | - [Credits](#credits) 18 | 19 | 20 | ## Build 21 | 22 | ```bash 23 | $ rebar3 compile 24 | ``` 25 | 26 | ## Test 27 | 28 | ```bash 29 | $ rebar3 test 30 | ``` 31 | 32 | ## Usage 33 | 34 | Add to you deps configuration in rebar.config for your project : 35 | 36 | ```erlang 37 | {deps, [{verl, "1.1.0"}]}. 38 | ``` 39 | 40 | ### Comparisons 41 | 42 | ```erlang 43 | 1> verl:compare(<<"1.0.0">>, <<"1.0.1">>). 44 | lt 45 | 2> verl:compare(<<"1.0.0">>, <<"1.0.0">>). 46 | eq 47 | 3> verl:compare(<<"2.0.0">>, <<"1.0.0">>). 48 | gt 49 | 4> verl:compare(<<"1.0.0-pre">>, <<"1.0.0">>). 50 | lt 51 | 5> verl:compare(<<"1.0.0">>, <<"1.0.0-pre">>). 52 | gt 53 | ``` 54 | 55 | ### Version, Requirements, and Matching 56 | 57 | #### Matching 58 | 59 | ```erlang 60 | 1> verl:is_match(<<"1.0.0">>, <<"~> 1.0.0">>). 61 | true 62 | 2> verl:is_match(<<"1.0.0">>, <<"~> 2.0.0">>). 63 | false 64 | 3> verl:is_match(<<"3.2.0">>, <<"~> 3.0.0">>). 65 | false 66 | 4> verl:is_match(<<"3.2.0">>, <<"~> 3.0">>). 67 | true 68 | ``` 69 | 70 | #### Compiled requirements for ludicious speed matching 71 | 72 | ```erlang 73 | 1> {ok, Req} = verl:parse_requirement(<<"~> 3.0">>). 74 | {ok,#{compiled => false, 75 | string => <<"~> 3.0">>, 76 | matchspec => [{{'$1','$2','$3','$4','$5'}...}], 77 | string => <<"~> 3.0">>}} 78 | 2> verl:is_match(<<"3.0.0-dev">>, Req). 79 | false 80 | 3> verl:is_match(<<"1.2.3">>, Req). 81 | false 82 | 4> verl:is_match(<<"3.1.0">>, Req). 83 | true 84 | ``` 85 | 86 | #### Version parsing 87 | 88 | ```erlang 89 | 1> verl:parse(<<"1.2.3">>). 90 | #{build => undefined,major => 1,minor => 2,patch => 3, 91 | pre => []} 92 | 2> verl:parse(<<"1.2.3+build">>). 93 | #{build => <<"build">>,major => 1,minor => 2,patch => 3, 94 | pre => []} 95 | 3> verl:parse(<<"1.2.3-pre+build">>). 96 | #{build => <<"build">>,major => 1,minor => 2,patch => 3, 97 | pre => [<<"pre">>]} 98 | 4> verl:parse(<<"1">>). 99 | {error, invalid_version} 100 | 5> verl:parse(<<"2">>). 101 | {error, invalid_version} 102 | ``` 103 | 104 | Don't want a map? Use the `verl_parser` module... 105 | 106 | ```erlang 107 | 1> verl_parser:parse_version(<<"1.2.3">>). 108 | {ok,{1,2,3,[],[]}} 109 | 2> verl_parser:parse_version(<<"1.2.3+build">>). 110 | {ok,{1,2,3,[],[<<"build">>]}} 111 | 3> verl_parser:parse_version(<<"1.2.3-pre+build">>). 112 | {ok,{1,2,3,[<<"pre">>],[<<"build">>]}} 113 | 4> verl_parser:parse_version(<<"1">>). 114 | {error, invalid_version} 115 | ``` 116 | 117 | ##### Requirements parsing 118 | 119 | ```erlang 120 | 1> verl:parse_requirement(<<"~> 2.1.0-dev">>). 121 | {ok,#{compiled => false, 122 | string => <<"~> 2.1.0-dev">>, 123 | matchspec => 124 | [{{'$1','$2','$3','$4','$5'}...] }} 125 | 2> verl:parse_requirement(<<"~> 2.1.0-">>). 126 | {error,invalid_requirement} 127 | ``` 128 | 129 | Don't want a map? User the `verl_parser` module... 130 | 131 | ```erlang 132 | 1> verl_parser:parse_requirement(<<"~> 2.1.0-dev">>). 133 | {ok, [{{'$1','$2','$3','$4','$5'}...]} 134 | 2> verl:parse_requirement(<<"~> 2.1.0-">>). 135 | {error,invalid_requirement} 136 | ``` 137 | 138 | ## Credits 139 | 140 | - All credit goes to the Elixir team and contributors to Version and 141 | Version.Parser in the Elixir standard lib for the algorithm and original 142 | implementation. 143 | -------------------------------------------------------------------------------- /docs.exs: -------------------------------------------------------------------------------- 1 | [ 2 | source_url: "https://github.com/jellybeam/verl", 3 | extras: ["README.md"], 4 | main: "readme", 5 | proglang: :erlang 6 | ] 7 | -------------------------------------------------------------------------------- /docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Setup: 5 | # 6 | # mix escript.install hex ex_doc 7 | # asdf install erlang 24.0-rc1 8 | # asdf local erlang 24.0-rc1 9 | 10 | rebar3 compile 11 | rebar3 edoc 12 | version=1.1.0 13 | ex_doc "verl" $version "_build/default/lib/verl/ebin" \ 14 | --source-ref v${version} \ 15 | --config docs.exs $@ 16 | -------------------------------------------------------------------------------- /elvis.config: -------------------------------------------------------------------------------- 1 | [{elvis, [ 2 | {config, [ 3 | #{ dirs => ["src/**"], 4 | filter => "*.erl", 5 | ruleset => erl_files }, 6 | #{ dirs => ["test/**"], 7 | filter => "*.erl", 8 | ruleset => erl_files }, 9 | #{ dirs => ["."], 10 | filter => "rebar.config", 11 | ruleset => rebar_config }, 12 | #{ dirs => ["."], 13 | filter => "elvis.config", 14 | ruleset => elvis_config } 15 | ]}, 16 | {verbose, true} 17 | ]}]. 18 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [ 2 | debug_info, 3 | warn_missing_spec, 4 | warnings_as_errors 5 | ]}. 6 | {minimum_otp_vsn, "19.3"}. 7 | {deps, []}. 8 | {project_plugins, [erlfmt, rebar3_proper, rebar3_hex, covertool, rebar3_lint, rebar3_hank]}. 9 | {profiles, [ 10 | {test, [ 11 | {deps, [{proper, "1.3.0"}]}, 12 | {erl_opts, [nowarn_missing_spec, nowarn_export_all]}, 13 | {dialyzer, [{plt_extra_apps, [proper]}]}, 14 | {cover_enabled, true}, 15 | {cover_opts, [verbose]} 16 | ]} 17 | ]}. 18 | 19 | {erlfmt, [ 20 | {files, "{src,include,test}/*.{hrl,erl}"} 21 | ]}. 22 | 23 | {edoc_opts, [ 24 | {doclet, edoc_doclet_chunks}, 25 | {layout, edoc_layout_chunks}, 26 | {preprocess, true}, 27 | {dir, "_build/default/lib/verl/doc"}]}. 28 | 29 | {xref_ignores, [verl, {verl_parser, parse_version, 2}]}. 30 | 31 | {alias, [{quick_test, [{proper, "--cover --numtests=3"}, 32 | {eunit, "--cover"}, 33 | {cover, "-v"}]}, 34 | {test, [{ct, "-c"}, {proper, "--cover"}, {eunit, "--cover"}, {cover, "-v"}]}, 35 | {check, [{proper, "--cover --numtests=3"}, 36 | {eunit, "--cover"}, 37 | xref, dialyzer, cover]}]}. 38 | 39 | {xref_checks,[undefined_function_calls,locals_not_used, 40 | deprecated_function_calls,exports_not_used]}. 41 | 42 | {dialyzer, [ 43 | {warnings, [ 44 | error_handling, 45 | unknown, 46 | unmatched_returns 47 | ]} 48 | ]}. 49 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /src/verl.app.src: -------------------------------------------------------------------------------- 1 | {application,verl, 2 | [{description,"SemVer2 version and requirements parsing, matching, and comparison"}, 3 | {vsn,git}, 4 | {organization,"jelly-beam"}, 5 | {registered,[]}, 6 | {applications,[kernel,stdlib]}, 7 | {env,[]}, 8 | {modules,[]}, 9 | {extra,{maintainers,["Bryan Paxton"]}}, 10 | {licenses,["Apache 2.0"]}, 11 | {links,[{"Github","https://github.com/jelly-beam/verl"}]}]}. 12 | -------------------------------------------------------------------------------- /src/verl.erl: -------------------------------------------------------------------------------- 1 | -module(verl). 2 | 3 | %% Main API 4 | -export([ 5 | compare/2, 6 | is_match/2, 7 | is_match/3, 8 | parse/1, 9 | parse_requirement/1, 10 | compile_requirement/1 11 | ]). 12 | 13 | %% Helpers 14 | -export([ 15 | between/3, 16 | eq/2, 17 | gt/2, 18 | gte/2, 19 | lt/2, 20 | lte/2 21 | ]). 22 | 23 | -type version() :: binary(). 24 | -type requirement() :: binary(). 25 | 26 | -type major() :: non_neg_integer(). 27 | -type minor() :: non_neg_integer(). 28 | -type patch() :: non_neg_integer(). 29 | -type pre() :: [binary() | non_neg_integer()]. 30 | -type build() :: binary() | undefined. 31 | -type version_t() :: #{ 32 | major => major(), 33 | minor => minor(), 34 | patch => patch(), 35 | pre => pre(), 36 | build => build() 37 | }. 38 | 39 | -type requirement_t() :: #{ 40 | string => requirement(), 41 | matchspec => list(), 42 | compiled => boolean() 43 | }. 44 | 45 | -type compiled_requirement() :: #{ 46 | compiled => true, 47 | matchspec => ets:comp_match_spec(), 48 | string => requirement() 49 | }. 50 | 51 | -type match_opts() :: [allow_pre | {allow_pre, true}]. 52 | 53 | -export_type([ 54 | version/0, 55 | requirement/0, 56 | major/0, 57 | minor/0, 58 | patch/0, 59 | pre/0, 60 | build/0, 61 | version_t/0, 62 | requirement_t/0, 63 | compiled_requirement/0 64 | ]). 65 | 66 | %%% Primary API 67 | 68 | %%% @doc 69 | %%% Compares two versions, returning whether the first argument is greater, equal, or 70 | %%% less than the second argument. 71 | %%% @end 72 | -spec compare(version(), version()) -> gt | eq | lt | {error, invalid_version}. 73 | compare(Version1, Version2) -> 74 | ver_cmp(to_matchable(Version1, true), to_matchable(Version2, true)). 75 | 76 | %%% @doc 77 | %%% Parses a semantic version, returning {ok, version_t()} or {error, invalid_version} 78 | %%% @end 79 | -spec parse(version()) -> {ok, version_t()} | {error, invalid_version}. 80 | parse(Str) -> 81 | build_version(Str). 82 | 83 | %%% @doc 84 | %%% Parses a semantic version requirement, returning {ok, requirement_t()} or 85 | %%% {error, invalid_requirement} 86 | %%% @end 87 | -spec parse_requirement(requirement()) -> {ok, requirement_t()} | {error, invalid_requirement}. 88 | parse_requirement(Str) -> 89 | case verl_parser:parse_requirement(Str) of 90 | {ok, Spec} -> 91 | {ok, #{string => Str, matchspec => Spec, compiled => false}}; 92 | {error, invalid_requirement} -> 93 | {error, invalid_requirement} 94 | end. 95 | 96 | %%% @doc 97 | %%% Compiles a version requirement as returned by `parse_requirement' for faster 98 | %%% matches. 99 | %%% @end 100 | -spec compile_requirement(requirement_t()) -> compiled_requirement(). 101 | compile_requirement(Req) when is_map(Req) -> 102 | Ms = ets:match_spec_compile(maps:get(matchspec, Req)), 103 | maps:put(compiled, true, maps:put(matchspec, Ms, Req)). 104 | 105 | %%% @doc 106 | %%% Returns `true' if the dependency is in range of the requirement, otherwise 107 | %%% `false', or an error. 108 | %%% @end 109 | -spec is_match(version() | version_t(), requirement() | requirement_t()) -> 110 | boolean() | {error, badarg | invalid_requirement | invalid_version}. 111 | is_match(Version, Requirement) -> 112 | is_match(Version, Requirement, []). 113 | 114 | %%% @doc 115 | %%% Works like `is_match/2' but takes extra options as an argument. 116 | %%% @end 117 | -spec is_match(version() | version_t(), requirement() | requirement_t(), match_opts()) -> 118 | boolean() | {error, badarg | invalid_requirement | invalid_version}. 119 | is_match(Version, Requirement, Opts) when is_binary(Version) andalso is_binary(Requirement) -> 120 | case build_version(Version) of 121 | {ok, Ver} -> 122 | case build_requirement(Requirement) of 123 | {ok, Req} -> 124 | is_match(Ver, Req, Opts); 125 | {error, invalid_requirement} -> 126 | {error, invalid_requirement} 127 | end; 128 | {error, invalid_version} -> 129 | {error, invalid_version} 130 | end; 131 | is_match(Version, Requirement, Opts) when is_binary(Version) andalso is_map(Requirement) -> 132 | case build_version(Version) of 133 | {ok, Ver} -> 134 | is_match(Ver, Requirement, Opts); 135 | {error, invalid_version} -> 136 | {error, invalid_version} 137 | end; 138 | is_match(Version, Requirement, Opts) when is_map(Version) andalso is_binary(Requirement) -> 139 | case build_requirement(Requirement) of 140 | {ok, Req} -> 141 | is_match(Version, Req, Opts); 142 | {error, invalid_requirement} -> 143 | {error, invalid_requirement} 144 | end; 145 | is_match(Version, #{matchspec := Spec, compiled := false} = R, Opts) when is_map(R) -> 146 | AllowPre = proplists:get_value(allow_pre, Opts, true), 147 | {ok, Result} = ets:test_ms(to_matchable(Version, AllowPre), Spec), 148 | Result /= false; 149 | is_match(Version, #{matchspec := Spec, compiled := true} = R, Opts) when 150 | is_map(Version) andalso is_map(R) 151 | -> 152 | AllowPre = proplists:get_value(allow_pre, Opts, true), 153 | ets:match_spec_run([to_matchable(Version, AllowPre)], Spec) /= []. 154 | 155 | to_matchable(#{major := Major, minor := Minor, patch := Patch, pre := Pre}, AllowPre) -> 156 | {Major, Minor, Patch, Pre, AllowPre}; 157 | to_matchable(String, AllowPre) when is_binary(String) -> 158 | case verl_parser:parse_version(String) of 159 | {ok, {Major, Minor, Patch, Pre, _Build}} -> 160 | {Major, Minor, Patch, Pre, AllowPre}; 161 | {error, invalid_version} -> 162 | {error, invalid_version} 163 | end. 164 | 165 | %%% Helper functions 166 | 167 | %%% @doc 168 | %%% Helper function that returns true if the first version is greater than the third version and 169 | %%% also the second version is less than the the third version, otherwise returns false. 170 | %%% See `compare/2' for more details. 171 | %%% @end 172 | -spec between(version(), version(), version()) -> boolean() | {error, invalid_version}. 173 | between(Vsn1, Vsn2, VsnMatch) -> 174 | case {gte(VsnMatch, Vsn1), lte(VsnMatch, Vsn2)} of 175 | {true, true} -> 176 | true; 177 | {{error, _} = Err, _} -> 178 | Err; 179 | {_, {error, _} = Err} -> 180 | Err; 181 | _ -> 182 | false 183 | end. 184 | 185 | %%% @doc 186 | %%% Helper function that returns true if two versions are equal, otherwise 187 | %%% false. See `compare/2' for more details. 188 | %%% @end 189 | -spec eq(version(), version()) -> boolean() | {error, invalid_version}. 190 | eq(Vsn1, Vsn2) -> 191 | case compare(Vsn1, Vsn2) of 192 | eq -> true; 193 | {error, _} = Err -> Err; 194 | _ -> false 195 | end. 196 | 197 | %%% @doc 198 | %%% Helper function that returns true the first version given is greater than 199 | %%% the second, otherwise returns false. See `compare/2' for more details. 200 | %%% @end 201 | -spec gt(version(), version()) -> boolean() | {error, invalid_version}. 202 | gt(Vsn1, Vsn2) -> 203 | case compare(Vsn1, Vsn2) of 204 | gt -> true; 205 | {error, _} = Err -> Err; 206 | _ -> false 207 | end. 208 | 209 | %%% @doc 210 | %%% Helper function that returns true the first version given is greater than 211 | %%% or equal to the second, otherwise returns false. 212 | %%% See `compare/2' for more details. 213 | %%% @end 214 | -spec gte(version(), version()) -> boolean() | {error, invalid_version}. 215 | gte(Vsn1, Vsn2) -> 216 | case compare(Vsn1, Vsn2) of 217 | gt -> true; 218 | eq -> true; 219 | {error, _} = Err -> Err; 220 | _ -> false 221 | end. 222 | 223 | %%% @doc 224 | %%% Helper function that returns true the first version given is less than the 225 | %%% second, otherwise returns false. See `compare/2' for more details. 226 | %%% @end 227 | -spec lt(version(), version()) -> boolean() | {error, invalid_version}. 228 | lt(Vsn1, Vsn2) -> 229 | case compare(Vsn1, Vsn2) of 230 | lt -> true; 231 | {error, _} = Err -> Err; 232 | _ -> false 233 | end. 234 | 235 | %%% @doc 236 | %%% Helper function that returns true the first version given is less than or 237 | %%% equal to the second, otherwise returns false. 238 | %%% See `compare/2' for more details. 239 | %%% @end 240 | -spec lte(version(), version()) -> boolean() | {error, invalid_version}. 241 | lte(Vsn1, Vsn2) -> 242 | case compare(Vsn1, Vsn2) of 243 | lt -> true; 244 | eq -> true; 245 | {error, _} = Err -> Err; 246 | _ -> false 247 | end. 248 | 249 | %% private api 250 | %% 251 | 252 | %% @private 253 | build_version(Version) -> 254 | case verl_parser:parse_version(Version) of 255 | {ok, {Major, Minor, Patch, Pre, Build}} -> 256 | {ok, #{ 257 | major => Major, 258 | minor => Minor, 259 | patch => Patch, 260 | pre => Pre, 261 | build => build_string(Build) 262 | }}; 263 | {error, invalid_version} -> 264 | {error, invalid_version} 265 | end. 266 | 267 | %% @private 268 | build_requirement(Str) -> 269 | case verl_parser:parse_requirement(Str) of 270 | {ok, Spec} -> 271 | {ok, #{string => Str, matchspec => Spec, compiled => false}}; 272 | {error, invalid_requirement} -> 273 | {error, invalid_requirement} 274 | end. 275 | 276 | %% @private 277 | build_string(Build) -> 278 | case Build of 279 | [] -> undefined; 280 | _ -> binary:list_to_bin(Build) 281 | end. 282 | 283 | %% @private 284 | ver_cmp({Maj1, Min1, Patch1, Pre1, _}, {Maj2, Min2, Patch2, Pre2, _}) -> 285 | case {Maj1, Min1, Patch1} > {Maj2, Min2, Patch2} of 286 | true -> 287 | gt; 288 | false -> 289 | case {Maj1, Min1, Patch1} < {Maj2, Min2, Patch2} of 290 | true -> 291 | lt; 292 | false -> 293 | test_pre(Pre1, Pre2) 294 | end 295 | end; 296 | ver_cmp(_, _) -> 297 | {error, invalid_version}. 298 | 299 | %% @private 300 | test_pre(Pre1, Pre2) -> 301 | case pre_is_eq(Pre1, Pre2) of 302 | true -> 303 | gt; 304 | false -> 305 | case pre_is_eq(Pre2, Pre1) of 306 | true -> 307 | lt; 308 | false -> 309 | pre_cmp(Pre1, Pre2) 310 | end 311 | end. 312 | 313 | %% @private 314 | pre_cmp(Pre1, Pre2) -> 315 | case Pre1 > Pre2 of 316 | true -> 317 | gt; 318 | false -> 319 | case Pre1 < Pre2 of 320 | true -> 321 | lt; 322 | false -> 323 | eq 324 | end 325 | end. 326 | 327 | %% @private 328 | pre_is_eq(Pre1, Pre2) -> 329 | case Pre1 == [] of 330 | false -> false; 331 | true -> Pre2 /= [] 332 | end. 333 | -------------------------------------------------------------------------------- /src/verl_parser.erl: -------------------------------------------------------------------------------- 1 | -module(verl_parser). 2 | 3 | -export([parse_requirement/1, parse_version/1, parse_version/2]). 4 | 5 | -type operator() :: '!=' | '&&' | '<' | '<=' | '==' | '>' | '>=' | '||' | '~>' | bitstring(). 6 | 7 | -spec parse_version(verl:version()) -> 8 | {ok, {verl:major(), verl:minor(), verl:patch(), verl:pre(), [verl:build()]}} 9 | | {error, invalid_version}. 10 | parse_version(Str) -> parse_version(Str, false). 11 | 12 | -spec parse_version(verl:version(), boolean()) -> 13 | {ok, {verl:major(), verl:minor(), verl:patch(), verl:pre(), [verl:build()]}} 14 | | {error, invalid_version}. 15 | parse_version(Str, Approximate) when is_binary(Str) -> 16 | try parse_and_convert(Str, Approximate) of 17 | {ok, {_, _, undefined, _, _}} -> 18 | {error, invalid_version}; 19 | {ok, _} = V -> 20 | V; 21 | {error, invalid_version} -> 22 | {error, invalid_version} 23 | catch 24 | error:{badmatch, {error, T}} when 25 | T =:= invalid_version orelse 26 | T =:= nan orelse 27 | T =:= bad_part orelse 28 | T =:= leading_zero 29 | -> 30 | {error, invalid_version} 31 | end. 32 | 33 | -spec parse_requirement(verl:requirement()) -> 34 | {ok, ets:match_spec()} | {error, invalid_requirement}. 35 | parse_requirement(Source) -> 36 | Lexed = lexer(Source, []), 37 | to_matchspec(Lexed). 38 | 39 | %% @private 40 | -spec lexer(binary(), [operator()]) -> [operator()]. 41 | lexer(<<">=", Rest/binary>>, Acc) -> 42 | lexer(Rest, ['>=' | Acc]); 43 | lexer(<<"<=", Rest/binary>>, Acc) -> 44 | lexer(Rest, ['<=' | Acc]); 45 | lexer(<<"~>", Rest/binary>>, Acc) -> 46 | lexer(Rest, ['~>' | Acc]); 47 | lexer(<<">", Rest/binary>>, Acc) -> 48 | lexer(Rest, ['>' | Acc]); 49 | lexer(<<"<", Rest/binary>>, Acc) -> 50 | lexer(Rest, ['<' | Acc]); 51 | lexer(<<"==", Rest/binary>>, Acc) -> 52 | lexer(Rest, ['==' | Acc]); 53 | lexer(<<"!=", Rest/binary>>, Acc) -> 54 | lexer(Rest, ['!=' | Acc]); 55 | lexer(<<"!", Rest/binary>>, Acc) -> 56 | lexer(Rest, ['!=' | Acc]); 57 | lexer(<<" or ", Rest/binary>>, Acc) -> 58 | lexer(Rest, ['||' | Acc]); 59 | lexer(<<" and ", Rest/binary>>, Acc) -> 60 | lexer(Rest, ['&&' | Acc]); 61 | lexer(<<" ", Rest/binary>>, Acc) -> 62 | lexer(Rest, Acc); 63 | lexer(<>, []) -> 64 | lexer(Rest, [<>, '==']); 65 | lexer(<>, [Head | Acc]) -> 66 | Acc1 = 67 | case Head of 68 | Head when is_binary(Head) -> 69 | [<> | Acc]; 70 | Head when Head =:= '&&' orelse Head =:= '||' -> 71 | [<>, '==', Head | Acc]; 72 | _Other -> 73 | [<>, Head | Acc] 74 | end, 75 | lexer(Body, Acc1); 76 | lexer(<<>>, Acc) -> 77 | lists:reverse(Acc). 78 | 79 | %% @private 80 | -spec parse_condition(verl:version()) -> 81 | {integer(), integer(), 'undefined' | integer(), [binary() | integer()]}. 82 | parse_condition(Version) -> parse_condition(Version, false). 83 | 84 | %% @private 85 | -spec parse_condition(verl:version(), boolean()) -> 86 | {integer(), integer(), 'undefined' | integer(), [binary() | integer()]}. 87 | parse_condition(Version, Approximate) -> 88 | try 89 | case parse_and_convert(Version, Approximate) of 90 | {ok, {Major, Minor, Patch, Pre, _Bld}} -> 91 | {Major, Minor, Patch, Pre}; 92 | _ -> 93 | throw(invalid_matchspec) 94 | end 95 | catch 96 | error:{badmatch, {error, T}} when 97 | T =:= invalid_version orelse 98 | T =:= nan orelse 99 | T =:= bad_part orelse 100 | T =:= leading_zero 101 | -> 102 | throw(invalid_matchspec) 103 | end. 104 | 105 | %% @private 106 | -spec approximate_upper({integer(), integer(), 'undefined' | integer(), [binary() | integer()]}) -> 107 | {integer(), integer(), 0, [0, ...]}. 108 | approximate_upper(Version) -> 109 | case Version of 110 | {Major, _Minor, undefined, _} -> 111 | {Major + 1, 0, 0, [0]}; 112 | {Major, Minor, _Patch, _Pre} -> 113 | {Major, Minor + 1, 0, [0]} 114 | end. 115 | 116 | %% @private 117 | -spec matchable_to_string( 118 | {integer(), integer(), 'undefined' | integer(), [binary() | integer()]} 119 | ) -> binary(). 120 | matchable_to_string({Major, Minor, Patch, Pre}) -> 121 | Patch1 = 122 | case Patch of 123 | P when P =:= undefined orelse P =:= false -> 124 | <<"0">>; 125 | _ -> 126 | maybe_to_string(Patch) 127 | end, 128 | Pre1 = 129 | case Pre == [] of 130 | true -> 131 | <<>>; 132 | false -> 133 | case Pre of 134 | [0] -> 135 | <<"-0">>; 136 | _ -> 137 | Pre0 = maybe_to_string(Pre), 138 | <<<<"-">>/binary, Pre0/binary>> 139 | end 140 | end, 141 | Major1 = maybe_to_string(Major), 142 | Minor1 = maybe_to_string(Minor), 143 | Patch2 = maybe_to_string(Patch1), 144 | Joined = join_bins([Major1, Minor1, Patch2], <<".">>), 145 | <>. 146 | 147 | %% @private 148 | -spec pre_condition('<' | '>', [binary() | integer()]) -> tuple(). 149 | pre_condition('>', Pre) -> 150 | PreLength = length(Pre), 151 | {'orelse', {'andalso', {'==', {length, '$4'}, 0}, {const, PreLength /= 0}}, 152 | {'andalso', {const, PreLength /= 0}, 153 | {'orelse', {'>', {length, '$4'}, PreLength}, 154 | {'andalso', {'==', {length, '$4'}, PreLength}, {'>', '$4', {const, Pre}}}}}}; 155 | pre_condition('<', Pre) -> 156 | PreLength = length(Pre), 157 | {'orelse', {'andalso', {'/=', {length, '$4'}, 0}, {const, PreLength == 0}}, 158 | {'andalso', {'/=', {length, '$4'}, 0}, 159 | {'orelse', {'<', {length, '$4'}, PreLength}, 160 | {'andalso', {'==', {length, '$4'}, PreLength}, {'<', '$4', {const, Pre}}}}}}. 161 | 162 | %% @private 163 | -spec no_pre_condition([binary() | integer()]) -> tuple(). 164 | no_pre_condition([]) -> 165 | {'orelse', '$5', {'==', {length, '$4'}, 0}}; 166 | no_pre_condition(_) -> 167 | {const, true}. 168 | 169 | %% @private 170 | -spec to_matchspec([operator(), ...]) -> {error, invalid_requirement} | {ok, ets:match_spec()}. 171 | to_matchspec(Lexed) -> 172 | try 173 | case is_valid_requirement(Lexed) of 174 | true -> 175 | First = to_condition(Lexed), 176 | Rest = lists:nthtail(2, Lexed), 177 | {ok, [{{'$1', '$2', '$3', '$4', '$5'}, [to_condition(First, Rest)], ['$_']}]}; 178 | false -> 179 | {error, invalid_requirement} 180 | end 181 | catch 182 | invalid_matchspec -> {error, invalid_requirement} 183 | end. 184 | 185 | %% @private 186 | -spec to_condition([iodata(), ...]) -> tuple(). 187 | to_condition(['==', Version | _]) -> 188 | Matchable = parse_condition(Version), 189 | main_condition('==', Matchable); 190 | to_condition(['!=', Version | _]) -> 191 | Matchable = parse_condition(Version), 192 | main_condition('/=', Matchable); 193 | to_condition(['~>', Version | _]) -> 194 | From = parse_condition(Version, true), 195 | To = approximate_upper(From), 196 | {'andalso', to_condition(['>=', matchable_to_string(From)]), 197 | to_condition(['<', matchable_to_string(To)])}; 198 | to_condition(['>', Version | _]) -> 199 | {Major, Minor, Patch, Pre} = 200 | parse_condition(Version), 201 | {'andalso', 202 | {'orelse', main_condition('>', {Major, Minor, Patch}), 203 | {'andalso', main_condition('==', {Major, Minor, Patch}), pre_condition('>', Pre)}}, 204 | no_pre_condition(Pre)}; 205 | to_condition(['>=', Version | _]) -> 206 | Matchable = parse_condition(Version), 207 | {'orelse', main_condition('==', Matchable), to_condition(['>', Version])}; 208 | to_condition(['<', Version | _]) -> 209 | {Major, Minor, Patch, Pre} = 210 | parse_condition(Version), 211 | {'orelse', main_condition('<', {Major, Minor, Patch}), 212 | {'andalso', main_condition('==', {Major, Minor, Patch}), pre_condition('<', Pre)}}; 213 | to_condition(['<=', Version | _]) -> 214 | Matchable = parse_condition(Version), 215 | {'orelse', main_condition('==', Matchable), to_condition(['<', Version])}. 216 | 217 | %% @private 218 | -spec to_condition(tuple(), list()) -> tuple(). 219 | to_condition(Current, []) -> 220 | Current; 221 | to_condition( 222 | Current, 223 | ['&&', Operator, Version | Rest] 224 | ) -> 225 | to_condition( 226 | {'andalso', Current, to_condition([Operator, Version])}, 227 | Rest 228 | ); 229 | to_condition( 230 | Current, 231 | ['||', Operator, Version | Rest] 232 | ) -> 233 | to_condition( 234 | {'orelse', Current, to_condition([Operator, Version])}, 235 | Rest 236 | ). 237 | 238 | %% @private 239 | -spec main_condition(any(), tuple()) -> tuple(). 240 | main_condition(Op, Version) when tuple_size(Version) == 3 -> 241 | {Op, {{'$1', '$2', '$3'}}, {const, Version}}; 242 | main_condition(Op, Version) when tuple_size(Version) == 4 -> 243 | {Op, {{'$1', '$2', '$3', '$4'}}, {const, Version}}. 244 | 245 | %% @private 246 | -spec bisect(binary(), binary(), list()) -> [binary() | undefined, ...]. 247 | bisect(Str, Delim, Opts) -> 248 | [First | Rest] = binary:split(Str, [Delim], Opts), 249 | Rest1 = 250 | case Rest of 251 | [] -> 252 | undefined; 253 | _ -> 254 | join_bins(Rest, Delim) 255 | end, 256 | [First, Rest1]. 257 | 258 | %% @private 259 | -spec has_leading_zero(error | undefined | binary() | [binary()]) -> boolean(). 260 | has_leading_zero(<<48/integer, _/integer, _/binary>>) -> 261 | true; 262 | has_leading_zero(_) -> 263 | false. 264 | 265 | %% @private 266 | -spec is_valid_identifier(any()) -> boolean(). 267 | is_valid_identifier(<>) when 268 | is_integer(Char) andalso 269 | Char >= 48 andalso Char =< 57; 270 | is_integer(Char) andalso 271 | Char >= 97 andalso Char =< 122; 272 | is_integer(Char) andalso 273 | Char >= 65 andalso Char =< 90; 274 | Char == 45 275 | -> 276 | is_valid_identifier(Rest); 277 | is_valid_identifier(<<>>) -> 278 | true; 279 | is_valid_identifier(_) -> 280 | false. 281 | 282 | %% @private 283 | -spec join_bins([binary(), ...], binary()) -> binary(). 284 | join_bins(List, Delim) -> 285 | lists:foldl( 286 | fun(Bin, Acc) -> 287 | case bit_size(Acc) of 288 | N when N > 0 -> 289 | <>; 290 | _ -> 291 | Bin 292 | end 293 | end, 294 | <<>>, 295 | List 296 | ). 297 | 298 | %% @private 299 | -spec maybe_patch(undefined | binary() | integer(), boolean()) -> {ok, undefined | integer()}. 300 | maybe_patch(undefined, true) -> 301 | {ok, undefined}; 302 | maybe_patch(Patch, _) -> 303 | to_digits(Patch). 304 | 305 | %% @private 306 | -spec parse_and_convert(verl:version(), boolean()) -> 307 | {error, invalid_version} 308 | | {ok, 309 | { 310 | integer(), 311 | integer(), 312 | 'undefined' 313 | | integer(), 314 | [ 315 | binary() 316 | | integer() 317 | ], 318 | [binary()] 319 | }}. 320 | parse_and_convert(Str, Approx) -> 321 | [VerPre, Build] = bisect(Str, <<"+">>, [global]), 322 | [Ver, Pre] = bisect(VerPre, <<"-">>, []), 323 | [Maj1, Min1, Patch1, Other] = split_ver(Ver), 324 | case Other of 325 | undefined -> 326 | {ok, Maj2} = to_digits(Maj1), 327 | {ok, Min2} = to_digits(Min1), 328 | {ok, Patch2} = maybe_patch(Patch1, Approx), 329 | {ok, PreParts} = opt_dot_separated(Pre), 330 | {ok, PreParts1} = parts_to_integers(PreParts, []), 331 | {ok, Build2} = opt_dot_separated(Build), 332 | {ok, {Maj2, Min2, Patch2, PreParts1, Build2}}; 333 | _ -> 334 | {error, invalid_version} 335 | end. 336 | 337 | %% @private 338 | -spec parse_digits('error' | 'undefined' | binary() | [binary()], bitstring()) -> 339 | {'error', 'nan'} | {'ok', integer()}. 340 | parse_digits(<>, Acc) when 341 | is_integer(Char) andalso Char >= 48 andalso Char =< 57 342 | -> 343 | parse_digits(Rest, <>); 344 | parse_digits(<<>>, Acc) when byte_size(Acc) > 0 -> 345 | {ok, binary_to_integer(Acc)}; 346 | parse_digits(_, _) -> 347 | {error, nan}. 348 | 349 | %% @private 350 | -spec parts_to_integers([binary()], [binary() | integer()]) -> 351 | {'error', 'nan'} | {'ok', [binary() | integer()]}. 352 | parts_to_integers([Part | Rest], Acc) -> 353 | case parse_digits(Part, <<>>) of 354 | {ok, Int} -> 355 | case has_leading_zero(Part) of 356 | P when P =:= undefined orelse P =:= false -> 357 | parts_to_integers(Rest, [Int | Acc]); 358 | _ -> 359 | {error, nan} 360 | end; 361 | {error, nan} -> 362 | parts_to_integers(Rest, [Part | Acc]) 363 | end; 364 | parts_to_integers([], Acc) -> 365 | {ok, lists:reverse(Acc)}. 366 | 367 | %% @private 368 | -spec opt_dot_separated('undefined' | binary()) -> {'error', 'bad_part'} | {'ok', [binary()]}. 369 | opt_dot_separated(undefined) -> 370 | {ok, []}; 371 | opt_dot_separated(Str) -> 372 | Parts = binary:split(Str, <<".">>, [global]), 373 | Fun = fun(P) -> 374 | case P /= <<>> of 375 | false -> false; 376 | true -> is_valid_identifier(P) 377 | end 378 | end, 379 | case lists:all(Fun, Parts) of 380 | P when P =:= undefined orelse P =:= false -> 381 | {error, bad_part}; 382 | _ -> 383 | {ok, Parts} 384 | end. 385 | 386 | %% @private 387 | -spec split_ver(binary()) -> ['error' | 'undefined' | binary() | [binary()], ...]. 388 | split_ver(Str) -> 389 | case binary:split(Str, [<<".">>], [global]) of 390 | [Maj0, Min0] -> 391 | [Maj0, Min0, undefined, undefined]; 392 | [Maj, Min, P] -> 393 | [Maj, Min, P, undefined]; 394 | [Major, Minor, Patch | Rest] -> 395 | [Major, Minor, Patch, Rest]; 396 | _ -> 397 | [error, error, error, error] 398 | end. 399 | 400 | %% @private 401 | -spec to_digits('error' | 'undefined' | binary() | [binary()]) -> 402 | {'error', 'leading_zero' | 'nan'} | {'ok', integer()}. 403 | to_digits(Str) -> 404 | case has_leading_zero(Str) of 405 | S when S =:= undefined orelse S =:= false -> 406 | parse_digits(Str, <<>>); 407 | true -> 408 | {error, leading_zero} 409 | end. 410 | 411 | %% @private 412 | -spec maybe_to_string(binary() | [binary() | byte()] | integer()) -> binary(). 413 | maybe_to_string(Part) -> 414 | case Part of 415 | Rewrite when is_binary(Rewrite) -> 416 | Rewrite; 417 | Int when is_integer(Int) -> 418 | integer_to_binary(Int); 419 | Rewrite when is_list(Rewrite) -> 420 | list_to_binary(Rewrite) 421 | end. 422 | 423 | %% @private 424 | -spec is_valid_requirement([operator(), ...]) -> boolean(). 425 | is_valid_requirement([]) -> false; 426 | is_valid_requirement([A | Next]) -> is_valid_requirement(A, Next). 427 | 428 | %% @private 429 | -spec is_valid_requirement(operator(), [operator()]) -> boolean(). 430 | is_valid_requirement(A, []) when is_binary(A) -> 431 | true; 432 | is_valid_requirement(A, [B | Next]) when 433 | (is_atom(A) andalso 434 | is_atom(B)) andalso 435 | (A =:= '&&' orelse A =:= '||') 436 | -> 437 | is_valid_requirement(B, Next); 438 | is_valid_requirement(A, [B | Next]) when 439 | (is_binary(A) andalso 440 | is_atom(B)) andalso 441 | (B =:= '&&' orelse B =:= '||') 442 | -> 443 | is_valid_requirement(B, Next); 444 | is_valid_requirement(A, [B | Next]) when 445 | (is_atom(A) andalso 446 | is_binary(B)) andalso 447 | (A =:= '&&' orelse A =:= '||') 448 | -> 449 | is_valid_requirement(B, Next); 450 | is_valid_requirement(A, [B | Next]) when 451 | is_atom(A) andalso 452 | is_binary(B) 453 | -> 454 | is_valid_requirement(B, Next); 455 | is_valid_requirement(_, _) -> 456 | false. 457 | -------------------------------------------------------------------------------- /test/prop_verl.erl: -------------------------------------------------------------------------------- 1 | -module(prop_verl). 2 | 3 | -include_lib("proper/include/proper.hrl"). 4 | -include_lib("stdlib/include/assert.hrl"). 5 | 6 | %%%%%%%%%%%%%%%%%% 7 | %%% Properties %%% 8 | %%%%%%%%%%%%%%%%%% 9 | 10 | % test for equality with opaque term 11 | -dialyzer({no_opaque, prop_basic_valid_semver0/0}). 12 | prop_basic_valid_semver0() -> 13 | ?FORALL( 14 | {Maj, Min, P, Pre}, 15 | {non_neg_integer(), non_neg_integer(), non_neg_integer(), non_empty(binary())}, 16 | begin 17 | Major = integer_to_binary(Maj), 18 | Minor = integer_to_binary(Min), 19 | Patch = integer_to_binary(P), 20 | V = 21 | <>/binary, Minor/binary, <<".">>/binary, Patch/binary, 22 | <<"-">>/binary, Pre/binary>>, 23 | case {re:run(Pre, "^[0-9A-Za-z-+]+$"), re:run(Pre, "(^0[0-9]+)|(^[+]$)|[\r\n]")} of 24 | {nomatch, _} -> 25 | {error, invalid_version} =:= verl:parse(V); 26 | {_, {match, _}} -> 27 | {error, invalid_version} =:= verl:parse(V); 28 | _ -> 29 | {ok, Parsed} = verl:parse(V), 30 | PreEl = 31 | case re:run(Pre, "^[0-9]+$") of 32 | nomatch -> 33 | [Pre]; 34 | _ -> 35 | [binary_to_integer(Pre)] 36 | end, 37 | Exp = #{ 38 | build => undefined, 39 | major => Maj, 40 | minor => Min, 41 | patch => P, 42 | pre => PreEl 43 | }, 44 | Exp =:= Parsed 45 | end 46 | end 47 | ). 48 | 49 | % test for equality with opaque term 50 | -dialyzer({no_opaque, prop_basic_valid_semver/0}). 51 | prop_basic_valid_semver() -> 52 | ?FORALL( 53 | {Maj, Min, P}, 54 | {non_neg_integer(), non_neg_integer(), non_neg_integer()}, 55 | begin 56 | Major = integer_to_binary(Maj), 57 | Minor = integer_to_binary(Min), 58 | Patch = integer_to_binary(P), 59 | V = <>/binary, Minor/binary, <<".">>/binary, Patch/binary>>, 60 | {ok, Parsed} = verl:parse(V), 61 | Exp = #{build => undefined, major => Maj, minor => Min, patch => P, pre => []}, 62 | Exp =:= Parsed 63 | end 64 | ). 65 | 66 | prop_basic_invalid_semver() -> 67 | ?FORALL( 68 | {Maj, Min, P}, 69 | {neg_integer(), neg_integer(), neg_integer()}, 70 | begin 71 | Major = integer_to_binary(Maj), 72 | Minor = integer_to_binary(Min), 73 | Patch = integer_to_binary(P), 74 | V = <>/binary, Minor/binary, <<".">>/binary, Patch/binary>>, 75 | {error, invalid_version} =:= verl:parse(V) 76 | end 77 | ). 78 | 79 | prop_basic_invalid_semver_more() -> 80 | ?FORALL( 81 | {Maj, Min, P}, 82 | {any(), any(), any()}, 83 | begin 84 | Major = term_to_binary(Maj), 85 | Minor = term_to_binary(Min), 86 | Patch = term_to_binary(P), 87 | V = <>/binary, Minor/binary, <<".">>/binary, Patch/binary>>, 88 | {error, invalid_version} =:= verl:parse(V) 89 | end 90 | ). 91 | 92 | prop_basic_invalid_semver_more2() -> 93 | ?FORALL( 94 | {Maj, Min, P, Pre}, 95 | {binary(), binary(), binary(), binary()}, 96 | begin 97 | V = 98 | <>/binary, Min/binary, <<".">>/binary, P/binary, <<"-">>/binary, 99 | Pre/binary>>, 100 | {error, invalid_version} =:= verl:parse(V) 101 | end 102 | ). 103 | 104 | %%%%%%%%%%%%%%% 105 | %%% Helpers %%% 106 | %%%%%%%%%%%%%%% 107 | 108 | %%%%%%%%%%%%%%%%%% 109 | %%% Generators %%% 110 | %%%%%%%%%%%%%%%%%% 111 | -------------------------------------------------------------------------------- /test/verl_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(verl_SUITE). 2 | 3 | -compile(export_all). 4 | -hank([{unnecessary_function_arguments, all}]). 5 | 6 | -include_lib("common_test/include/ct.hrl"). 7 | -include_lib("stdlib/include/assert.hrl"). 8 | 9 | all() -> 10 | [ 11 | between_test, 12 | compare_test, 13 | eq_test, 14 | gt_test, 15 | gte_test, 16 | lt_test, 17 | lte_test, 18 | parse_test, 19 | parse_requirement_test, 20 | compile_requirement_test, 21 | is_match_test 22 | ]. 23 | 24 | between_test(_Config) -> 25 | ?assertMatch( 26 | true, 27 | verl:between( 28 | <<"1.0.0-alpha">>, 29 | <<"1.0.0-alpha.3">>, 30 | <<"1.0.0-alpha.2">> 31 | ) 32 | ), 33 | ?assertMatch( 34 | true, 35 | verl:between( 36 | <<"1.0.0-alpha.1">>, 37 | <<"1.0.0-beta.2">>, 38 | <<"1.0.0-alpha.25">> 39 | ) 40 | ), 41 | ?assertMatch( 42 | true, 43 | verl:between( 44 | <<"1.0.0-beta.2">>, 45 | <<"1.0.0-beta.11">>, 46 | <<"1.0.0-beta.7">> 47 | ) 48 | ), 49 | ?assertMatch( 50 | true, 51 | verl:between( 52 | <<"1.0.0-pre-alpha.2">>, 53 | <<"1.0.0-pre-alpha.11">>, 54 | <<"1.0.0-pre-alpha.7">> 55 | ) 56 | ), 57 | ?assertMatch( 58 | true, 59 | verl:between( 60 | <<"1.0.0-beta.11">>, 61 | <<"1.0.0-rc.3">>, 62 | <<"1.0.0-rc.1">> 63 | ) 64 | ), 65 | ?assertMatch( 66 | true, 67 | verl:between( 68 | <<"1.0.0-rc.1">>, 69 | <<"1.0.0-rc.1+build.3">>, 70 | <<"1.0.0-rc.1+build.1">> 71 | ) 72 | ), 73 | 74 | ?assertMatch( 75 | true, 76 | verl:between( 77 | <<"1.0.0-rc.1+build.1">>, 78 | <<"1.0.0">>, 79 | <<"1.0.0-rc.33">> 80 | ) 81 | ), 82 | ?assertMatch( 83 | true, 84 | verl:between( 85 | <<"1.0.0">>, 86 | <<"1.0.0+0.3.7">>, 87 | <<"1.0.0+0.2">> 88 | ) 89 | ), 90 | ?assertMatch( 91 | true, 92 | verl:between( 93 | <<"1.3.7+build">>, 94 | <<"1.3.7+build.2.b8f12d7">>, 95 | <<"1.3.7+build.1">> 96 | ) 97 | ), 98 | ?assertMatch( 99 | true, 100 | verl:between( 101 | <<"1.3.7+build.2.b8f12d7">>, 102 | <<"1.3.7+build.11.e0f985a">>, 103 | <<"1.3.7+build.10.a36faa">> 104 | ) 105 | ), 106 | ?assertMatch( 107 | true, 108 | verl:between( 109 | <<"1.0.0-alpha">>, 110 | <<"1.0.0-alpha">>, 111 | <<"1.0.0-alpha">> 112 | ) 113 | ), 114 | ?assertMatch( 115 | true, 116 | not verl:between( 117 | <<"1.0.0-alpha.1">>, 118 | <<"1.0.0-alpha.22">>, 119 | <<"1.0.0">> 120 | ) 121 | ), 122 | ?assertMatch( 123 | true, 124 | not verl:between( 125 | <<"1.0.0-pre-alpha.1">>, 126 | <<"1.0.0-pre-alpha.22">>, 127 | <<"1.0.0">> 128 | ) 129 | ), 130 | ?assertMatch( 131 | true, 132 | not verl:between( 133 | <<"1.0.0-beta.1">>, 134 | <<"1.0.0-beta.11">>, 135 | <<"1.0.0-alpha">> 136 | ) 137 | ), 138 | ?assertMatch( 139 | true, 140 | not verl:between( 141 | <<"1.0.0-beta.11">>, 142 | <<"1.0.0-rc.1">>, 143 | <<"1.0.0-rc.22">> 144 | ) 145 | ). 146 | 147 | compare_test(_Cfg) -> 148 | gt = verl:compare(<<"1.0.1">>, <<"1.0.0">>), 149 | gt = verl:compare(<<"1.1.0">>, <<"1.0.1">>), 150 | gt = verl:compare(<<"2.1.1">>, <<"1.2.2">>), 151 | gt = verl:compare(<<"1.0.0">>, <<"1.0.0-dev">>), 152 | gt = verl:compare(<<"1.2.3-dev">>, <<"0.1.2">>), 153 | gt = verl:compare(<<"1.0.0-a.b">>, <<"1.0.0-a">>), 154 | gt = verl:compare(<<"1.0.0-b">>, <<"1.0.0-a.b">>), 155 | gt = verl:compare(<<"1.0.0-a">>, <<"1.0.0-0">>), 156 | gt = verl:compare(<<"1.0.0-a.b">>, <<"1.0.0-a.a">>), 157 | lt = verl:compare(<<"1.0.0">>, <<"1.0.1">>), 158 | lt = verl:compare(<<"1.0.1">>, <<"1.1.0">>), 159 | lt = verl:compare(<<"1.2.2">>, <<"2.1.1">>), 160 | lt = verl:compare(<<"1.0.0-dev">>, <<"1.0.0">>), 161 | lt = verl:compare(<<"0.1.2">>, <<"1.2.3-dev">>), 162 | lt = verl:compare(<<"1.0.0-a">>, <<"1.0.0-a.b">>), 163 | lt = verl:compare(<<"1.0.0-a.b">>, <<"1.0.0-b">>), 164 | lt = verl:compare(<<"1.0.0-0">>, <<"1.0.0-a">>), 165 | lt = verl:compare(<<"1.0.0-a.a">>, <<"1.0.0-a.b">>), 166 | eq = verl:compare(<<"1.0.0">>, <<"1.0.0">>), 167 | eq = verl:compare(<<"1.0.0-dev">>, <<"1.0.0-dev">>), 168 | eq = verl:compare(<<"1.0.0-a">>, <<"1.0.0-a">>), 169 | {error, invalid_version} = verl:compare(<<"1.0">>, <<"1.0.0">>), 170 | {error, invalid_version} = verl:compare(<<"1.0.0-dev">>, <<"1.0">>), 171 | {error, invalid_version} = verl:compare(<<"foo">>, <<"1.0.0-a">>). 172 | 173 | eq_test(_Config) -> 174 | ?assertMatch(true, verl:eq(<<"1.0.0-a">>, <<"1.0.0-a">>)), 175 | ?assertMatch(true, verl:eq(<<"1.0.0-alpha">>, <<"1.0.0-alpha">>)), 176 | ?assertMatch(true, verl:eq(<<"1.0.0-dev">>, <<"1.0.0-dev">>)), 177 | ?assertMatch(true, not verl:eq(<<"1.0.0">>, <<"1.0.1">>)), 178 | ?assertMatch(true, not verl:eq(<<"1.0.0-alpha">>, <<"1.0.1+alpha">>)), 179 | ?assertMatch(true, not verl:eq(<<"1.0.0+build.1">>, <<"1.0.1+build.2">>)). 180 | 181 | gt_test(_Config) -> 182 | ?assertMatch( 183 | true, 184 | verl:gt( 185 | <<"1.0.0-alpha.1">>, 186 | <<"1.0.0-alpha">> 187 | ) 188 | ), 189 | ?assertMatch( 190 | true, 191 | verl:gt( 192 | <<"1.0.0-beta.2">>, 193 | <<"1.0.0-alpha.1">> 194 | ) 195 | ), 196 | ?assertMatch( 197 | true, 198 | verl:gt( 199 | <<"1.0.0-beta.11">>, 200 | <<"1.0.0-beta.2">> 201 | ) 202 | ), 203 | ?assertMatch( 204 | true, 205 | verl:gt( 206 | <<"1.0.0-pre-alpha.14">>, 207 | <<"1.0.0-pre-alpha.3">> 208 | ) 209 | ), 210 | ?assertMatch(true, verl:gt(<<"1.0.0-rc.1">>, <<"1.0.0-beta.11">>)), 211 | ?assertMatch(true, verl:gt(<<"1.0.0">>, <<"1.0.0-rc.1+build.1">>)), 212 | ?assertMatch(true, verl:gt(<<"1.3.7+build">>, <<"1.0.0+0.3.7">>)), 213 | ?assertMatch( 214 | true, 215 | not verl:gt( 216 | <<"1.0.0-alpha">>, 217 | <<"1.0.0-alpha.1">> 218 | ) 219 | ), 220 | ?assertMatch( 221 | true, 222 | not verl:gt( 223 | <<"1.0.0-alpha.1">>, 224 | <<"1.0.0-beta.2">> 225 | ) 226 | ), 227 | ?assertMatch( 228 | true, 229 | not verl:gt( 230 | <<"1.0.0-beta.2">>, 231 | <<"1.0.0-beta.11">> 232 | ) 233 | ), 234 | ?assertMatch( 235 | true, 236 | not verl:gt( 237 | <<"1.0.0-beta.11">>, 238 | <<"1.0.0-rc.1">> 239 | ) 240 | ), 241 | ?assertMatch( 242 | true, 243 | not verl:gt( 244 | <<"1.0.0-pre-alpha.3">>, 245 | <<"1.0.0-pre-alpha.14">> 246 | ) 247 | ), 248 | ?assertMatch( 249 | true, 250 | not verl:gt( 251 | <<"1.0.0-rc.1">>, 252 | <<"1.0.0-rc.1+build.1">> 253 | ) 254 | ), 255 | ?assertMatch( 256 | true, 257 | not verl:gt( 258 | <<"1.0.0-rc.1+build.1">>, 259 | <<"1.0.0">> 260 | ) 261 | ), 262 | ?assertMatch( 263 | true, 264 | not verl:gt( 265 | <<"1.0.0">>, 266 | <<"1.0.0+0.3.7">> 267 | ) 268 | ), 269 | ?assertMatch( 270 | true, 271 | not verl:gt( 272 | <<"1.0.0+0.3.7">>, 273 | <<"1.3.7+build">> 274 | ) 275 | ), 276 | ?assertMatch( 277 | true, 278 | not verl:gt( 279 | <<"1.3.7+build">>, 280 | <<"1.3.7+build.2.b8f12d7">> 281 | ) 282 | ), 283 | ?assertMatch( 284 | true, 285 | not verl:gt( 286 | <<"1.3.7+build.2.b8f12d7">>, 287 | <<"1.3.7+build.11.e0f985a">> 288 | ) 289 | ), 290 | ?assertMatch( 291 | true, 292 | not verl:gt( 293 | <<"1.0.0-alpha">>, 294 | <<"1.0.0-alpha">> 295 | ) 296 | ). 297 | 298 | % match against opaque 299 | -dialyzer({[no_opaque, no_return], parse_test/1}). 300 | parse_test(_Cfg) -> 301 | Exp0 = #{major => 1, minor => 2, patch => 3, pre => [], build => undefined}, 302 | {ok, Exp0} = verl:parse(<<"1.2.3">>), 303 | Exp1 = #{major => 1, minor => 4, patch => 5, pre => [], build => <<"ignore">>}, 304 | {ok, Exp1} = verl:parse(<<"1.4.5+ignore">>), 305 | Exp2 = #{major => 0, minor => 0, patch => 1, pre => [], build => <<"sha0702245">>}, 306 | {ok, Exp2} = verl:parse(<<"0.0.1+sha.0702245">>), 307 | Exp3 = #{major => 1, minor => 4, patch => 5, pre => [<<"6-g3318bd5">>], build => undefined}, 308 | {ok, Exp3} = verl:parse(<<"1.4.5-6-g3318bd5">>), 309 | Exp4 = #{major => 1, minor => 4, patch => 5, pre => [6, 7, <<"eight">>], build => undefined}, 310 | {ok, Exp4} = verl:parse(<<"1.4.5-6.7.eight">>), 311 | Exp5 = #{major => 1, minor => 4, patch => 5, pre => [<<"6-g3318bd5">>], build => <<"ignore">>}, 312 | {ok, Exp5} = verl:parse(<<"1.4.5-6-g3318bd5+ignore">>), 313 | ExpErr = {error, invalid_version}, 314 | ExpErr = verl:parse(<<"foobar">>), 315 | ExpErr = verl:parse(<<"2">>), 316 | ExpErr = verl:parse(<<"2.">>), 317 | ExpErr = verl:parse(<<"2.3">>), 318 | ExpErr = verl:parse(<<"2.3.">>), 319 | ExpErr = verl:parse(<<"2.3.0-">>), 320 | ExpErr = verl:parse(<<"2.3.0+">>), 321 | ExpErr = verl:parse(<<"2.3.0.">>), 322 | ExpErr = verl:parse(<<"2.3.0.4">>), 323 | ExpErr = verl:parse(<<"2.3.-rc.1">>), 324 | ExpErr = verl:parse(<<"2.3.+rc.1">>), 325 | ExpErr = verl:parse(<<"2.3.0-01">>), 326 | ExpErr = verl:parse(<<"2.3.00-1">>), 327 | ExpErr = verl:parse(<<"2.3.00">>), 328 | ExpErr = verl:parse(<<"2.03.0">>), 329 | ExpErr = verl:parse(<<"02.3.0">>). 330 | 331 | % match against opaque 332 | -dialyzer({[no_opaque, no_return], parse_requirement_test/1}). 333 | parse_requirement_test(_Cfg) -> 334 | Str = <<"1.2.3">>, 335 | ExpSpec = [ 336 | { 337 | {'$1', '$2', '$3', '$4', '$5'}, 338 | [{'==', {{'$1', '$2', '$3', '$4'}}, {const, {1, 2, 3, []}}}], 339 | ['$_'] 340 | } 341 | ], 342 | {ok, #{string := Str, matchspec := ExpSpec, compiled := false}} = 343 | verl:parse_requirement(Str), 344 | ExpErr = {error, invalid_requirement}, 345 | ExpErr = verl:parse_requirement(<<"1">>), 346 | ExpErr = verl:parse_requirement(<<"1.2">>), 347 | ExpErr = verl:parse_requirement(<<"1.2-3">>), 348 | ExpErr = verl:parse_requirement(<<"_ 1.2.3">>), 349 | ExpErr = verl:parse_requirement(<<"( ) 1.2.3">>). 350 | 351 | % match against opaque 352 | -dialyzer({[no_opaque, no_return], compile_requirement_test/1}). 353 | compile_requirement_test(_Cfg) -> 354 | {ok, Req} = verl:parse_requirement(<<"1.2.3">>), 355 | #{compiled := true, matchspec := Ref} = verl:compile_requirement(Req), 356 | 357 | {Ver, []} = string:to_integer(erlang:system_info(otp_release)), 358 | case Ver of 359 | N when N < 20 -> 360 | true = is_binary(Ref); 361 | N when N >= 20 -> 362 | true = is_reference(Ref) 363 | end. 364 | 365 | % opaque as arg 366 | -dialyzer({[no_opaque, no_return], is_match_test/1}). 367 | is_match_test(_Cfg) -> 368 | {error, invalid_version} = verl:is_match(<<"foo">>, <<"2.3.0">>), 369 | {error, invalid_requirement} = verl:is_match(<<"2.3.0">>, <<"foo">>), 370 | true = verl:is_match(<<"2.3.0">>, <<"== 2.3.0">>), 371 | true = verl:is_match(<<"2.3.0">>, <<"~> 2.3.0">>), 372 | true = verl:is_match(<<"1.2.3-alpha">>, <<"1.2.3-alpha">>), 373 | true = verl:is_match(<<"0.9.3">>, <<"== 0.9.3+dev">>), 374 | true = verl:is_match(<<"2.3.0">>, <<"2.3.0">>), 375 | false = verl:is_match(<<"2.4.0">>, <<"2.3.0">>), 376 | false = verl:is_match(<<"2.3.0">>, <<"!= 2.3.0">>), 377 | false = verl:is_match(<<"2.3.0">>, <<"<= 2.2.0">>), 378 | {ok, Ver} = verl:parse(<<"2.3.0">>), 379 | {ok, Req} = verl:parse_requirement(<<"2.3.0">>), 380 | {error, invalid_version} = verl:is_match(<<"foo">>, Req), 381 | true = verl:is_match(Ver, Req), 382 | true = verl:is_match(<<"2.3.0">>, Req), 383 | true = verl:is_match(Ver, <<"2.3.0">>), 384 | {error, invalid_requirement} = verl:is_match(Ver, <<"= 2.3.0">>), 385 | true = verl:is_match(Ver, Req, []), 386 | {error, invalid_version} = verl:is_match(<<".3.0">>, Req, []), 387 | true = verl:is_match(Ver, <<"== 2.3.0">>, []), 388 | true = verl:is_match(<<"2.3.0">>, Req, []), 389 | {error, invalid_version} = verl:is_match(<<"0">>, <<"== 2.3.0">>, []), 390 | {error, invalid_requirement} = verl:is_match(Ver, <<"= 2.3.0">>, []), 391 | {error, invalid_requirement} = verl:is_match(<<"2.3.0">>, <<"= 2.3.0">>, []), 392 | true = verl:is_match(<<"2.3.0">>, <<"== 2.3.0">>, []), 393 | Compiled = verl:compile_requirement(Req), 394 | true = verl:is_match(Ver, Compiled, []), 395 | true = verl:is_match(<<"2.4.0">>, <<"!2.3.0">>), 396 | false = verl:is_match(<<"2.3.0">>, <<"!2.3.0">>), 397 | true = verl:is_match(<<"2.4.0">>, <<"!= 2.3.0">>), 398 | false = verl:is_match(<<"2.3.0">>, <<"!= 2.3.0">>), 399 | true = verl:is_match(<<"2.4.0">>, <<"> 2.3.0">>), 400 | false = verl:is_match(<<"2.2.0">>, <<"> 2.3.0">>), 401 | false = verl:is_match(<<"2.3.0">>, <<"> 2.3.0">>), 402 | 403 | true = verl:is_match(<<"1.2.3">>, <<"> 1.2.3-alpha">>), 404 | true = verl:is_match(<<"1.2.3-alpha.1">>, <<"> 1.2.3-alpha">>), 405 | true = verl:is_match(<<"1.2.3-alpha.beta.sigma">>, <<"> 1.2.3-alpha.beta">>), 406 | false = verl:is_match(<<"1.2.3-alpha.10">>, <<"< 1.2.3-alpha.1">>), 407 | false = verl:is_match(<<"0.10.2-dev">>, <<"> 0.10.2">>), 408 | 409 | true = verl:is_match(<<"2.4.0">>, <<">= 2.3.0">>), 410 | false = verl:is_match(<<"2.2.0">>, <<">= 2.3.0">>), 411 | true = verl:is_match(<<"2.3.0">>, <<">= 2.3.0">>), 412 | true = verl:is_match(<<"2.0.0">>, <<">= 1.0.0">>), 413 | true = verl:is_match(<<"1.0.0">>, <<"1.0.0">>), 414 | 415 | true = verl:is_match(<<"2.2.0">>, <<"< 2.3.0">>), 416 | false = verl:is_match(<<"2.4.0">>, <<"< 2.3.0">>), 417 | false = verl:is_match(<<"2.3.0">>, <<"< 2.3.0">>), 418 | true = verl:is_match(<<"0.10.2-dev">>, <<"< 0.10.2">>), 419 | false = verl:is_match(<<"1.0.0">>, <<"< 1.0.0-dev">>), 420 | false = verl:is_match(<<"1.2.3-dev">>, <<"< 0.1.2">>), 421 | 422 | true = verl:is_match(<<"2.2.0">>, <<"<= 2.3.0">>), 423 | false = verl:is_match(<<"2.4.0">>, <<"<= 2.3.0">>), 424 | true = verl:is_match(<<"2.3.0">>, <<"<= 2.3.0">>), 425 | 426 | true = verl:is_match(<<"3.0.0">>, <<"~> 3.0">>), 427 | true = verl:is_match(<<"3.2.0">>, <<"~> 3.0">>), 428 | false = verl:is_match(<<"4.0.0">>, <<"~> 3.0">>), 429 | false = verl:is_match(<<"4.4.0">>, <<"~> 3.0">>), 430 | 431 | true = verl:is_match(<<"3.0.2">>, <<"~> 3.0.0">>), 432 | true = verl:is_match(<<"3.0.0">>, <<"~> 3.0.0">>), 433 | false = verl:is_match(<<"3.1.0">>, <<"~> 3.0.0">>), 434 | false = verl:is_match(<<"3.4.0">>, <<"~> 3.0.0">>), 435 | 436 | true = verl:is_match(<<"3.6.0">>, <<"~> 3.5">>), 437 | true = verl:is_match(<<"3.5.0">>, <<"~> 3.5">>), 438 | false = verl:is_match(<<"4.0.0">>, <<"~> 3.5">>), 439 | false = verl:is_match(<<"5.0.0">>, <<"~> 3.5">>), 440 | 441 | true = verl:is_match(<<"3.5.2">>, <<"~> 3.5.0">>), 442 | true = verl:is_match(<<"3.5.4">>, <<"~> 3.5.0">>), 443 | false = verl:is_match(<<"3.6.0">>, <<"~> 3.5.0">>), 444 | false = verl:is_match(<<"3.6.3">>, <<"~> 3.5.0">>), 445 | 446 | true = verl:is_match(<<"0.9.3">>, <<"~> 0.9.3-dev">>), 447 | false = verl:is_match(<<"0.10.0">>, <<"~> 0.9.3-dev">>), 448 | 449 | false = verl:is_match(<<"0.3.0-dev">>, <<"~> 0.2.0">>), 450 | 451 | false = verl:is_match(<<"2.2.0-dev">>, <<"~> 2.1.0">>), 452 | false = verl:is_match(<<"2.2.0-dev">>, <<"~> 2.1.0">>, [{allow_pre, false}]), 453 | false = verl:is_match(<<"2.2.0-dev">>, <<"~> 2.1.0-dev">>), 454 | false = verl:is_match(<<"2.2.0-dev">>, <<"~> 2.1.0-dev">>, [{allow_pre, false}]). 455 | 456 | gte_test(_Config) -> 457 | ?assertMatch(true, verl:gte(<<"1.0.0-alpha">>, <<"1.0.0-alpha">>)), 458 | ?assertMatch(true, verl:gte(<<"1.0.0-pre-alpha.2">>, <<"1.0.0-pre-alpha">>)), 459 | ?assertMatch(true, verl:gte(<<"1.0.0-beta.2">>, <<"1.0.0-alpha.1">>)), 460 | ?assertMatch(true, verl:gte(<<"1.0.0-rc.1">>, <<"1.0.0-beta.11">>)), 461 | ?assertMatch(true, verl:gte(<<"1.0.0-rc.1+build.1">>, <<"1.0.0-rc.1">>)), 462 | ?assertMatch(true, verl:gte(<<"1.0.0">>, <<"1.0.0-rc.1+build.1">>)), 463 | ?assertMatch(true, verl:gte(<<"1.0.0+0.3.7">>, <<"1.0.0">>)), 464 | ?assertMatch(true, verl:gte(<<"1.3.7+build">>, <<"1.0.0+0.3.7">>)), 465 | ?assertMatch(true, verl:gte(<<"1.3.7+build.2.b8f12d7">>, <<"1.3.7+build">>)), 466 | ?assertMatch(true, verl:gte(<<"1.3.7+build.11.e0f985a">>, <<"1.3.7+build.2.b8f12d7">>)), 467 | ?assertMatch(true, not verl:gte(<<"1.0.0-alpha">>, <<"1.0.0-alpha.1">>)), 468 | ?assertMatch(true, not verl:gte(<<"1.0.0-pre-alpha">>, <<"1.0.0-pre-alpha.1">>)), 469 | ?assertMatch(true, not verl:gte(<<"1.0.0-alpha.1">>, <<"1.0.0-beta.2">>)), 470 | ?assertMatch(true, not verl:gte(<<"1.0.0-beta.2">>, <<"1.0.0-beta.11">>)), 471 | ?assertMatch(true, not verl:gte(<<"1.0.0-beta.11">>, <<"1.0.0-rc.1">>)), 472 | ?assertMatch(true, not verl:gte(<<"1.0.0-rc.1+build.1">>, <<"1.0.0">>)), 473 | ?assertMatch(true, not verl:gte(<<"1.0.0+0.3.7">>, <<"1.3.7+build">>)). 474 | 475 | lt_test(_Config) -> 476 | ?assertMatch( 477 | true, 478 | verl:lt( 479 | <<"1.0.0-alpha">>, 480 | <<"1.0.0-alpha.1">> 481 | ) 482 | ), 483 | ?assertMatch( 484 | true, 485 | verl:lt( 486 | <<"1.0.0-alpha.1">>, 487 | <<"1.0.0-beta.2">> 488 | ) 489 | ), 490 | ?assertMatch( 491 | true, 492 | verl:lt( 493 | <<"1.0.0-beta.2">>, 494 | <<"1.0.0-beta.11">> 495 | ) 496 | ), 497 | ?assertMatch( 498 | true, 499 | verl:lt( 500 | <<"1.0.0-pre-alpha.3">>, 501 | <<"1.0.0-pre-alpha.14">> 502 | ) 503 | ), 504 | ?assertMatch( 505 | true, 506 | verl:lt( 507 | <<"1.0.0-beta.11">>, 508 | <<"1.0.0-rc.1">> 509 | ) 510 | ), 511 | ?assertMatch( 512 | true, 513 | verl:lt( 514 | <<"1.0.0-rc.1+build.1">>, 515 | <<"1.0.0">> 516 | ) 517 | ), 518 | ?assertMatch( 519 | true, 520 | verl:lt( 521 | <<"1.0.0+0.3.7">>, 522 | <<"1.3.7+build">> 523 | ) 524 | ), 525 | ?assertMatch( 526 | true, 527 | not verl:lt( 528 | <<"1.0.0-alpha">>, 529 | <<"1.0.0-alpha">> 530 | ) 531 | ), 532 | ?assertMatch( 533 | true, 534 | not verl:lt( 535 | <<"1.0.0-alpha.1">>, 536 | <<"1.0.0-alpha">> 537 | ) 538 | ), 539 | ?assertMatch( 540 | true, 541 | not verl:lt( 542 | <<"1.0.0-beta.2">>, 543 | <<"1.0.0-alpha.1">> 544 | ) 545 | ), 546 | ?assertMatch( 547 | true, 548 | not verl:lt( 549 | <<"1.0.0-beta.11">>, 550 | <<"1.0.0-beta.2">> 551 | ) 552 | ), 553 | ?assertMatch( 554 | true, 555 | not verl:lt( 556 | <<"1.0.0-pre-alpha.14">>, 557 | <<"1.0.0-pre-alpha.3">> 558 | ) 559 | ), 560 | ?assertMatch(true, not verl:lt(<<"1.0.0-rc.1">>, <<"1.0.0-beta.11">>)), 561 | ?assertMatch(true, not verl:lt(<<"1.0.0-rc.1+build.1">>, <<"1.0.0-rc.1">>)), 562 | ?assertMatch(true, not verl:lt(<<"1.0.0">>, <<"1.0.0-rc.1+build.1">>)), 563 | ?assertMatch(true, not verl:lt(<<"1.0.0+0.3.7">>, <<"1.0.0">>)), 564 | ?assertMatch(true, not verl:lt(<<"1.3.7+build">>, <<"1.0.0+0.3.7">>)), 565 | ?assertMatch( 566 | true, 567 | not verl:lt( 568 | <<"1.3.7+build.2.b8f12d7">>, 569 | <<"1.3.7+build">> 570 | ) 571 | ), 572 | ?assertMatch( 573 | true, 574 | not verl:lt( 575 | <<"1.3.7+build.11.e0f985a">>, 576 | <<"1.3.7+build.2.b8f12d7">> 577 | ) 578 | ). 579 | 580 | lte_test(_Config) -> 581 | ?assertMatch( 582 | true, 583 | verl:lte( 584 | <<"1.0.0-alpha">>, 585 | <<"1.0.0-alpha.1">> 586 | ) 587 | ), 588 | ?assertMatch( 589 | true, 590 | verl:lte( 591 | <<"1.0.0-alpha.1">>, 592 | <<"1.0.0-beta.2">> 593 | ) 594 | ), 595 | ?assertMatch( 596 | true, 597 | verl:lte( 598 | <<"1.0.0-beta.2">>, 599 | <<"1.0.0-beta.11">> 600 | ) 601 | ), 602 | ?assertMatch( 603 | true, 604 | verl:lte( 605 | <<"1.0.0-pre-alpha.2">>, 606 | <<"1.0.0-pre-alpha.11">> 607 | ) 608 | ), 609 | ?assertMatch( 610 | true, 611 | verl:lte( 612 | <<"1.0.0-beta.11">>, 613 | <<"1.0.0-rc.1">> 614 | ) 615 | ), 616 | ?assertMatch( 617 | true, 618 | verl:lte( 619 | <<"1.0.0-rc.1">>, 620 | <<"1.0.0-rc.1+build.1">> 621 | ) 622 | ), 623 | ?assertMatch( 624 | true, 625 | verl:lte( 626 | <<"1.0.0-rc.1+build.1">>, 627 | <<"1.0.0">> 628 | ) 629 | ), 630 | ?assertMatch( 631 | true, 632 | verl:lte( 633 | <<"1.0.0">>, 634 | <<"1.0.0+0.3.7">> 635 | ) 636 | ), 637 | ?assertMatch( 638 | true, 639 | verl:lte( 640 | <<"1.0.0+0.3.7">>, 641 | <<"1.3.7+build">> 642 | ) 643 | ), 644 | ?assertMatch( 645 | true, 646 | verl:lte( 647 | <<"1.3.7+build">>, 648 | <<"1.3.7+build.2.b8f12d7">> 649 | ) 650 | ), 651 | ?assertMatch( 652 | true, 653 | verl:lte( 654 | <<"1.3.7+build.2.b8f12d7">>, 655 | <<"1.3.7+build.11.e0f985a">> 656 | ) 657 | ), 658 | ?assertMatch( 659 | true, 660 | verl:lte( 661 | <<"1.0.0-alpha">>, 662 | <<"1.0.0-alpha">> 663 | ) 664 | ), 665 | ?assertMatch( 666 | true, 667 | not verl:lte( 668 | <<"1.0.0-alpha.1">>, 669 | <<"1.0.0-alpha">> 670 | ) 671 | ), 672 | ?assertMatch( 673 | true, 674 | not verl:lte( 675 | <<"1.0.0-pre-alpha.2">>, 676 | <<"1.0.0-pre-alpha">> 677 | ) 678 | ), 679 | ?assertMatch( 680 | true, 681 | not verl:lte( 682 | <<"1.0.0-beta.2">>, 683 | <<"1.0.0-alpha.1">> 684 | ) 685 | ), 686 | ?assertMatch( 687 | true, 688 | not verl:lte( 689 | <<"1.0.0-beta.11">>, 690 | <<"1.0.0-beta.2">> 691 | ) 692 | ), 693 | ?assertMatch(true, not verl:lte(<<"1.0.0-rc.1">>, <<"1.0.0-beta.11">>)), 694 | ?assertMatch(true, not verl:lte(<<"1.0.0">>, <<"1.0.0-rc.1+build.1">>)), 695 | ?assertMatch(true, not verl:lte(<<"1.3.7+build">>, <<"1.0.0+0.3.7">>)). 696 | -------------------------------------------------------------------------------- /test/verl_parser_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(verl_parser_SUITE). 2 | 3 | -compile(export_all). 4 | -hank([{unnecessary_function_arguments, all}]). 5 | 6 | -include_lib("common_test/include/ct.hrl"). 7 | 8 | all() -> 9 | [parse_version_test, parse_requirement_test]. 10 | 11 | % match against opaque 12 | -dialyzer({[no_opaque, no_return], parse_version_test/1}). 13 | parse_version_test(_Cfg) -> 14 | {ok, {1, 2, 3, [], []}} = verl_parser:parse_version(<<"1.2.3">>), 15 | {ok, {1, 4, 5, [], [<<"ignore">>]}} = verl_parser:parse_version(<<"1.4.5+ignore">>), 16 | {ok, {0, 0, 1, [], [<<"sha">>, <<"0702245">>]}} = verl_parser:parse_version( 17 | <<"0.0.1+sha.0702245">> 18 | ), 19 | {ok, {1, 4, 5, [<<"6-g3318bd5">>], []}} = 20 | verl_parser:parse_version(<<"1.4.5-6-g3318bd5">>), 21 | {ok, {1, 4, 5, [6, 7, <<"eight">>], []}} = 22 | verl_parser:parse_version(<<"1.4.5-6.7.eight">>), 23 | {ok, {1, 4, 5, [<<"6-g3318bd5">>], [<<"ignore">>]}} = 24 | verl_parser:parse_version(<<"1.4.5-6-g3318bd5+ignore">>), 25 | ExpError = {error, invalid_version}, 26 | ExpError = verl_parser:parse_version(<<"foobar">>), 27 | ExpError = verl_parser:parse_version(<<"2">>), 28 | ExpError = verl_parser:parse_version(<<"2.">>), 29 | ExpError = verl_parser:parse_version(<<"2.3">>), 30 | ExpError = verl_parser:parse_version(<<"2.3.">>), 31 | ExpError = verl_parser:parse_version(<<"2.3.0-">>), 32 | ExpError = verl_parser:parse_version(<<"2.3.0+">>), 33 | ExpError = verl_parser:parse_version(<<"2.3.0.">>), 34 | ExpError = verl_parser:parse_version(<<"2.3.0.4">>), 35 | ExpError = verl_parser:parse_version(<<"2.3.-rc.1">>), 36 | ExpError = verl_parser:parse_version(<<"2.3.+rc.1">>), 37 | ExpError = verl_parser:parse_version(<<"2.3.0-01">>), 38 | ExpError = verl_parser:parse_version(<<"2.3.00-1">>), 39 | ExpError = verl_parser:parse_version(<<"2.3.00">>), 40 | ExpError = verl_parser:parse_version(<<"2.03.0">>), 41 | ExpError = verl_parser:parse_version(<<"02.3.0">>), 42 | ExpError = verl_parser:parse_version(<<"0. 0.0">>), 43 | ExpError = verl_parser:parse_version(<<"0.1.0-&&pre">>). 44 | 45 | parse_requirement_test(_Cfg) -> 46 | ExpSpec0 = [ 47 | { 48 | {'$1', '$2', '$3', '$4', '$5'}, 49 | [{'==', {{'$1', '$2', '$3', '$4'}}, {const, {1, 2, 3, []}}}], 50 | ['$_'] 51 | } 52 | ], 53 | {ok, ExpSpec0} = verl_parser:parse_requirement(<<"1.2.3">>), 54 | ExpSpec1 = [ 55 | { 56 | {'$1', '$2', '$3', '$4', '$5'}, 57 | [{'/=', {{'$1', '$2', '$3', '$4'}}, {const, {1, 2, 3, []}}}], 58 | ['$_'] 59 | } 60 | ], 61 | {ok, ExpSpec1} = verl_parser:parse_requirement(<<"!= 1.2.3">>), 62 | {ok, _} = verl_parser:parse_requirement(<<"~> 1.2.3">>), 63 | {ok, _} = verl_parser:parse_requirement(<<"<= 1.2.3">>), 64 | ExpErr = {error, invalid_requirement}, 65 | ExpErr = verl_parser:parse_requirement(<<>>), 66 | ExpErr = verl_parser:parse_requirement(<<"and 2.1.0 and 2.1.1">>), 67 | ExpErr = verl_parser:parse_requirement(<<"2.1.1 or">>), 68 | ExpErr = verl_parser:parse_requirement(<<" and !">>), 69 | ExpErr = verl_parser:parse_requirement(<<" ! and">>). 70 | -------------------------------------------------------------------------------- /vendor.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | if [[ -z "$1" || -z "$2" ]]; then 5 | echo "Usage: vendor.sh TARGET_DIR PREFIX" 6 | exit 1 7 | fi 8 | 9 | source_dir=`dirname $0`/src 10 | target_dir=$1 11 | prefix=$2 12 | verl_version=`cat $source_dir/verl.hrl | grep VERL_VERSION | cut -d'"' -f2` 13 | 14 | filenames="verl.hrl \ 15 | verl.erl \ 16 | verl_parser.erl" 17 | 18 | search_to_replace="verl" 19 | 20 | for filename in $filenames; do 21 | rm -f $target_dir/$prefix$filename 22 | source_path=$source_dir/$filename 23 | target_path=$target_dir/$prefix$filename 24 | 25 | echo "%% Vendored from verl v$verl_version, do not edit manually" > $target_path 26 | echo >> $target_path 27 | cat $source_path >> $target_path 28 | 29 | for word in $search_to_replace; do 30 | sed -i.bak s/$word/$prefix$word/g $target_path 31 | rm $target_path.bak 32 | done 33 | done 34 | --------------------------------------------------------------------------------