Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

Add 'hexpm/' from commit 'cf59310c4752d3d77c7d574d41025e2b61678a28'

git-subtree-dir: hexpm
git-subtree-mainline: ea62f68bdb0debea402d03fbaf53ecb60533ac8f
git-subtree-split: cf59310c4752d3d77c7d574d41025e2b61678a28

+4775
+69
hexpm/.github/workflows/ci.yml
··· 1 + name: ci 2 + 3 + on: 4 + pull_request: 5 + push: 6 + branches: 7 + - main 8 + workflow_dispatch: 9 + 10 + permissions: 11 + contents: read 12 + 13 + concurrency: 14 + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 15 + cancel-in-progress: true 16 + 17 + jobs: 18 + test: 19 + name: test 20 + runs-on: ${{ matrix.os }} 21 + strategy: 22 + matrix: 23 + toolchain: [stable] 24 + build: [linux-amd64, macos, windows] 25 + include: 26 + - build: linux-amd64 27 + os: ubuntu-latest 28 + target: x86_64-unknown-linux-gnu 29 + - build: macos 30 + os: macos-latest 31 + target: x86_64-apple-darwin 32 + - build: windows 33 + os: windows-latest 34 + target: x86_64-pc-windows-msvc 35 + steps: 36 + - name: Checkout repository 37 + uses: actions/checkout@v4 38 + 39 + - name: Install Rust toolchain 40 + uses: dtolnay/rust-toolchain@stable 41 + with: 42 + toolchain: ${{ matrix.toolchain }} 43 + target: ${{ matrix.target }} 44 + 45 + - name: Run tests 46 + uses: clechasseur/rs-cargo@v2 47 + with: 48 + command: test 49 + args: --workspace --target ${{ matrix.target }} 50 + 51 + format-lint: 52 + name: format-lint 53 + runs-on: ubuntu-latest 54 + timeout-minutes: 10 55 + steps: 56 + - name: Checkout repository 57 + uses: actions/checkout@v4 58 + 59 + - name: Install Rust toolchain 60 + uses: dtolnay/rust-toolchain@stable 61 + with: 62 + toolchain: stable 63 + components: clippy, rustfmt 64 + 65 + - name: Check formatting 66 + run: cargo fmt --all -- --check 67 + 68 + - name: Run linter 69 + run: cargo clippy --workspace
+2
hexpm/.gitignore
··· 1 + /target 2 + Cargo.lock
+131
hexpm/CHANGELOG.md
··· 1 + # Changelog 2 + 3 + ## v5.1.1 - 2025-12-01 4 + 5 + - Fixed a bug with request path construction. 6 + 7 + ## v5.1.0 - 2025-11-06 8 + 9 + - Updated dependencies. 10 + 11 + ## v5.0.1 - 2025-11-06 12 + 13 + - Fixed a bug where ranges with `~>` and `and` or `or` would fail to parse. 14 + 15 + ## v5.0.0 - 2025-09-19 16 + 17 + - Functions renamed to make it more clear if the JSON API is used or if the 18 + repository endpoints are used. 19 + - Added `repository_v2_package_parse_body`, and `repository_v2_get_versions_body`, 20 + 21 + ## v4.2.0 - 2025-08-27 22 + 23 + - `Version`'s serde deserializer can now work with `String` as well as `str`. 24 + 25 + ## v4.1.0 - 2025-06-03 26 + 27 + - Added `impl From<Range> for pubgrub::Range<Version>` 28 + 29 + ## v4.0.0 - 2025-05-09 30 + 31 + - Removed `version::{pubgrub_report, Version::bump, PackageVersions, resolve_versions, PackageFetcher}`. 32 + 33 + ## v3.3.0 - 2025-03-10 34 + 35 + - Replaced protobuf dependency with prost to avoid a security vulnerability. 36 + 37 + ## v3.2.0 - 2025-03-07 38 + 39 + - Updated protobuf to 3.7.1 and regenerated code from .proto files 40 + 41 + ## v3.1.0 - 2024-06-23 42 + 43 + - Added specific errors for missing minor or patch versions. 44 + 45 + ## v3.0.0 - 2024-05-30 46 + 47 + - Added `add_owner_request`, `add_owner_response` 48 + `transfer_owner_request`, `transfer_owner_response`, 49 + 50 + - `revert_package_request`, and `revert_package_response` 51 + are renamed to `remove_package_request`, and `remove_package_response` 52 + to accurately describe what they do. 53 + 54 + ## v2.4.1 - 2024-05-01 55 + 56 + - Fixed a bug where prerelease versions would not `bump` correctly. This caused 57 + a panic in the Gleam compiler. 58 + 59 + ## v2.4.0 - 2024-05-01 60 + 61 + - Added the `revert_release_request`, `revert_release_response`, 62 + `revert_package_request`, and `revert_package_response` functions. 63 + 64 + ## v2.3.0 - 2024-04-26 65 + 66 + - Updated all dependencies. 67 + 68 + ## v2.2.0 - 2024-04-10 69 + 70 + - Updated for latest version of Reqwest. 71 + 72 + ## v2.1.1 - 2023-11-02 73 + 74 + - Fixed a bug where the package name validation regex was too strict. 75 + 76 + ## v2.1.0 - 2023-08-30 77 + 78 + - Updated for latest Hex API. 79 + 80 + ## v2.0.0 - 2022-04-12 81 + 82 + - The `publish_package_request` now takes an additional argument signifying 83 + whether an existing package can be replaced for now. 84 + 85 + ## v1.4.1 - 2021-12-03 86 + 87 + - Fixed a bug where locked versions would discard incompatible requirements 88 + during version resolution. 89 + 90 + ## v1.4.0 - 2021-11-25 91 + 92 + - The HTTP client has been removed from this library in favour of request 93 + building and response parsing functions. Bring your own HTTP client of choice. 94 + - `Version` and `Requirement` structs have been added for representing 95 + information about requests versions of packages. 96 + - Protobuf code updated to use `cfg_attr` rather than deprecated attributes. 97 + - The `get_repository_versions` method returngit@github.com:gleam-lang/hexpm-rust.gits `Version`s rather than strings. 98 + - The `get_package` uses the `version::Range` type to represent `requirement`s. 99 + - A pubgrub based dependency resolver has been added. 100 + - Fixed a bug where a forbidden error was returned for a not-found package by 101 + the get-package API. 102 + - Swapped the word "token" for "key" to match Hex. 103 + - Added `unretire_release_*`, `retire_release_*`, `remove_api_key_*` and 104 + `publish_package_*` functions. 105 + 106 + ## v1.3.0 - 2020-03-30 107 + 108 + - The `get_package_tarball` method has been added. 109 + 110 + ## v1.2.0 - 2020-01-17 111 + 112 + - The `get_package` method has been added. 113 + - Update tokio to 1.0 114 + - Update reqwest to 0.11 115 + - Update url to 2.2 116 + - Update bytes to 1.0 117 + - The generated protobuf code has been regenerated. 118 + 119 + ## v1.1.1 - 2020-07-20 120 + 121 + - Updated generated protobuffer deserialisation code. 122 + 123 + ## v1.1.0 - 2020-07-20 124 + 125 + - The `get_repository_versions` method has been added. 126 + - The `repository_base` field has been added to both authenticated and 127 + unauthenticated clients. 128 + 129 + ## v1.0.0 - 2020-05-03 130 + 131 + - Initial release
+60
hexpm/CONTRIBUTING.md
··· 1 + # How to Contribute 2 + 3 + ## Adding a New API Function 4 + 5 + 1. Figure out what you want to do 6 + - Go to https://hexdocs.pm/hex/Mix.Tasks.Hex.html and find what you want to do 7 + 2. Once you find the page, click on the code icon on the top-right to go to the corresponding source code like so: https://github.com/hexpm/hex/blob/main/lib/mix/tasks/hex.owner.ex#L125 8 + ```elixir 9 + defp transfer_owner(organization, package, owner) do 10 + auth = Mix.Tasks.Hex.auth_info(:write) 11 + Hex.Shell.info("Transferring ownership to #{owner} for #{package}") 12 + 13 + case Hex.API.Package.Owner.add(organization, package, owner, "full", true, auth) do 14 + {:ok, {code, _body, _headers}} when code in 200..299 -> 15 + :ok 16 + 17 + other -> 18 + Hex.Shell.error("Transferring ownership failed") 19 + Hex.Utils.print_error_result(other) 20 + end 21 + end 22 + ``` 23 + 24 + 3. The API function this is calling is `Hex.API.Package.Owner.add` so we go to https://github.com/hexpm/hex/blob/main/lib/hex/api/package.ex, scroll down to the defmodule for Owner and then find the `add` function: 25 + ```elixir 26 + def add(repo, package, owner, level, transfer, auth) when package != "" do 27 + Hex.API.check_write_api() 28 + 29 + owner = URI.encode_www_form(owner) 30 + path = "packages/#{URI.encode(package)}/owners/#{URI.encode(owner)}" 31 + params = %{level: level, transfer: transfer} 32 + API.erlang_put_request(repo, path, params, auth) 33 + end 34 + ``` 35 + `path` tells us what path and variables we will be using while `params` tells us what the body of the request should contain. The `API.erlang_put_request` at the bottom of this tells us the method of our request needs to be `PUT`. Now we have all of the information we need to create our request function like so: 36 + ```elixir 37 + pub fn transfer_owner_request( 38 + package_name: &str, 39 + owner: &str, 40 + api_key: &str, 41 + config: &Config, 42 + ) -> http::Request<Vec<u8>> { 43 + let body = json!({ 44 + "level": OwnerLevel::Full.to_string(), 45 + "transfer": true, 46 + }); 47 + 48 + config 49 + .api_request( 50 + Method::PUT, 51 + &format!("packages/{}/owners/{}", package_name, owner), 52 + Some(api_key), 53 + ) 54 + .body(body.to_string().into_bytes()) 55 + .expect("transfer_owner_request request") 56 + } 57 + ``` 58 + Note that the `api_key` and `config` fields will always be present in these request functions while the other fields are tailored to the specific request we want to make. 59 + 60 + 4. TODO: How to figure out what to write for the response function?
+56
hexpm/Cargo.toml
··· 1 + [package] 2 + name = "hexpm" 3 + version = "5.1.1" 4 + authors = ["Louis Pilfold <louis@lpil.uk>"] 5 + edition = "2024" 6 + 7 + readme = "README.md" 8 + license = "Apache-2.0" 9 + repository = "https://github.com/gleam-lang/hexpm-rust" 10 + description = "A Rust client for the Hex package manager" 11 + keywords = ["erlang", "gleam", "elixir", "hex", "api-client"] 12 + categories = ["api-bindings"] 13 + 14 + [dependencies] 15 + # Derive Error trait 16 + thiserror = "2" 17 + # JSON (de)serialization 18 + serde = { version = "1.0", features = ["derive"] } 19 + serde_json = "1.0" 20 + # HTTP types 21 + url = "2.2" 22 + http = "1.0" 23 + # Complex static values 24 + lazy_static = "1.4" 25 + # Text parsing with regular expressions 26 + regex = "1.3" 27 + # Byte collections 28 + bytes = "1" 29 + # gzip (de)compression 30 + flate2 = "1.0" 31 + # RSA signature and SHA256 checksum verification 32 + ring = "0.17" 33 + # PEM -> DER conversion 34 + x509-parser = "0.18" 35 + # Pubgrub dependency resolution algorithm 36 + pubgrub = "0.3" 37 + # Basic auth HTTP helper 38 + http-auth-basic = "0.3" 39 + # base16 encoding 40 + base16 = { version = "0.2", features = ["alloc"] } 41 + # Protobuf runtime 42 + prost = "0.13.5" 43 + 44 + [dev-dependencies] 45 + # HTTP client 46 + reqwest = { version = "0.12", features = ["json"] } 47 + # HTTP mock server 48 + mockito = "1.4" 49 + # Async runtime 50 + tokio = { version = "1", features = ["full"] } 51 + # toml encoding 52 + toml = "0.8" 53 + 54 + [build-dependencies] 55 + # Protobuf codegen 56 + prost-build = "0.13.5"
+211
hexpm/LICENCE
··· 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 2020 - present Louis Pilfold 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 + 203 + 204 + 205 + ## Runtime Library Exception to the Apache 2.0 License: ## 206 + 207 + 208 + As an exception, if you use this Software to compile your source code and 209 + portions of this Software are embedded into the binary product as a result, 210 + you may redistribute such product without providing attribution as would 211 + otherwise be required by Sections 4(a), 4(b) and 4(d) of the License.
+16
hexpm/README.md
··· 1 + # hexpm-rust 2 + 3 + ![license](https://img.shields.io/crates/l/hexpm.svg) 4 + [![crates.io](https://img.shields.io/crates/v/hexpm.svg?logo=rust)][crates] 5 + [![docs.rs](https://img.shields.io/badge/docs.rs-hexpm-blue)][docs] 6 + 7 + A Rust client for [Hex][hex], the package manager for the Erlang ecosystem. 8 + 9 + This library was created for use in the [Gleam programming language][gleam] 10 + compiler. The API is not overly well considered and breaking API changes may 11 + occur depending on the needs of the Gleam compiler. 12 + 13 + [hex]: https://hex.pm/ 14 + [gleam]: https://gleam.run/ 15 + [crates]: https://crates.io/crates/hexpm 16 + [docs]: https://docs.rs/hexpm
+23
hexpm/build.rs
··· 1 + /// `prost_build` generates files in the output directory, which means that if we want 2 + /// to use it, we would need the protoc compiler as a build dependency. 3 + /// To get around this, we need run the build script and manually copy the generated files 4 + /// into the `src` folder. To do so, uncomment the below lines, then copy the files from the 5 + /// output directory into the `src/proto` folder. The path to the output directory 6 + /// will be printed in the terminal. 7 + /// 8 + fn main() { 9 + // prost_build::compile_protos( 10 + // &[ 11 + // "proto/signed.proto", 12 + // "proto/package.proto", 13 + // "proto/versions.proto", 14 + // ], 15 + // &["proto/"], 16 + // ) 17 + // .expect("Failed to generate prost code from .proto files"); 18 + 19 + // println!( 20 + // "cargo::warning=Regenerated proto files, which must be manually copied into the `src` directory. Generated files can be found in {}", 21 + // std::env::var("OUT_DIR").unwrap_or_default() 22 + // ); 23 + }
+18
hexpm/proto/names.proto
··· 1 + syntax = "proto2"; 2 + 3 + package names; 4 + 5 + message Names { 6 + // All packages in the repository 7 + repeated Package packages = 1; 8 + // Name of repository 9 + required string repository = 2; 10 + } 11 + 12 + message Package { 13 + // Package name 14 + required string name = 1; 15 + 16 + // If set, the name of the package repository (NEVER USED, DEPRECATED) 17 + // string repository = 2; 18 + }
+55
hexpm/proto/package.proto
··· 1 + syntax = "proto2"; 2 + 3 + package package; 4 + 5 + message Package { 6 + // All releases of the package 7 + repeated Release releases = 1; 8 + // Name of package 9 + required string name = 2; 10 + // Name of repository 11 + required string repository = 3; 12 + } 13 + 14 + message Release { 15 + // Release version 16 + required string version = 1; 17 + // sha256 checksum of "inner" package tarball 18 + // deprecated in favor of outer_checksum 19 + required bytes inner_checksum = 2; 20 + // All dependencies of the release 21 + repeated Dependency dependencies = 3; 22 + // If set the release is retired, a retired release should only be 23 + // resolved if it has already been locked in a project 24 + optional RetirementStatus retired = 4; 25 + // sha256 checksum of outer package tarball 26 + // required when encoding but optional when decoding 27 + optional bytes outer_checksum = 5; 28 + } 29 + 30 + message RetirementStatus { 31 + required RetirementReason reason = 1; 32 + optional string message = 2; 33 + } 34 + 35 + enum RetirementReason { 36 + RETIRED_OTHER = 0; 37 + RETIRED_INVALID = 1; 38 + RETIRED_SECURITY = 2; 39 + RETIRED_DEPRECATED = 3; 40 + RETIRED_RENAMED = 4; 41 + } 42 + 43 + message Dependency { 44 + // Package name of dependency 45 + required string package = 1; 46 + // Version requirement of dependency 47 + required string requirement = 2; 48 + // If set and true the package is optional (see dependency resolution) 49 + optional bool optional = 3; 50 + // If set is the OTP application name of the dependency, if not set the 51 + // application name is the same as the package name 52 + optional string app = 4; 53 + // If set, the repository where the dependency is located 54 + optional string repository = 5; 55 + }
+10
hexpm/proto/signed.proto
··· 1 + syntax = "proto2"; 2 + 3 + package signed; 4 + 5 + message Signed { 6 + // Signed contents 7 + required bytes payload = 1; 8 + // The signature 9 + optional bytes signature = 2; 10 + }
+21
hexpm/proto/versions.proto
··· 1 + syntax = "proto2"; 2 + 3 + package versions; 4 + 5 + message Versions { 6 + // All packages in the repository 7 + repeated VersionsPackage packages = 1; 8 + // Name of repository 9 + required string repository = 2; 10 + } 11 + 12 + message VersionsPackage { 13 + // Package name 14 + required string name = 1; 15 + // All released versions of the package 16 + repeated string versions = 2; 17 + // Zero-based indexes of retired versions in the versions field, see package.proto 18 + repeated int32 retired = 3 [packed=true]; 19 + // If set, the name of the package repository (NEVER USED, DEPRECATED) 20 + // string repository = 4; 21 + }
+1034
hexpm/src/lib.rs
··· 1 + mod proto; 2 + 3 + #[cfg(test)] 4 + mod tests; 5 + 6 + pub mod version; 7 + 8 + use crate::proto::{signed::Signed, versions::Versions}; 9 + use bytes::buf::Buf; 10 + use flate2::read::GzDecoder; 11 + use http::{Method, StatusCode}; 12 + use lazy_static::lazy_static; 13 + use prost::Message; 14 + use regex::Regex; 15 + use ring::digest::{Context, SHA256}; 16 + use serde::Deserialize; 17 + use serde_json::json; 18 + use std::{ 19 + collections::HashMap, 20 + convert::{TryFrom, TryInto}, 21 + fmt::Display, 22 + io::{BufReader, Read}, 23 + }; 24 + use thiserror::Error; 25 + use version::{Range, Version}; 26 + use x509_parser::prelude::FromDer; 27 + 28 + #[derive(Debug, Clone)] 29 + pub struct Config { 30 + /// Defaults to https://hex.pm/api/ 31 + pub api_base: http::Uri, 32 + /// Defaults to https://repo.hex.pm/ 33 + pub repository_base: http::Uri, 34 + } 35 + 36 + impl Config { 37 + pub fn new() -> Self { 38 + Self { 39 + api_base: http::Uri::from_static("https://hex.pm/api/"), 40 + repository_base: http::Uri::from_static("https://repo.hex.pm/"), 41 + } 42 + } 43 + 44 + fn api_request( 45 + &self, 46 + method: http::Method, 47 + path_suffix: &str, 48 + api_key: Option<&str>, 49 + ) -> http::request::Builder { 50 + make_request(self.api_base.clone(), method, path_suffix, api_key) 51 + .header("content-type", "application/json") 52 + .header("accept", "application/json") 53 + } 54 + 55 + fn repository_request( 56 + &self, 57 + method: http::Method, 58 + path_suffix: &str, 59 + api_key: Option<&str>, 60 + ) -> http::request::Builder { 61 + make_request(self.repository_base.clone(), method, path_suffix, api_key) 62 + } 63 + } 64 + impl Default for Config { 65 + fn default() -> Self { 66 + Self::new() 67 + } 68 + } 69 + 70 + fn make_request( 71 + base: http::Uri, 72 + method: http::Method, 73 + path_suffix: &str, 74 + api_key: Option<&str>, 75 + ) -> http::request::Builder { 76 + let mut parts = base.into_parts(); 77 + parts.path_and_query = Some( 78 + match parts.path_and_query { 79 + Some(path_and_query) => { 80 + let mut path = path_and_query.path().to_owned(); 81 + if !path.ends_with('/') { 82 + path.push('/'); 83 + } 84 + path += path_suffix; 85 + 86 + // Drop query parameters 87 + path.try_into() 88 + } 89 + None => path_suffix.try_into(), 90 + } 91 + .expect("api_uri path"), 92 + ); 93 + let uri = http::Uri::from_parts(parts).expect("api_uri building"); 94 + let mut builder = http::Request::builder() 95 + .method(method) 96 + .uri(uri) 97 + .header("user-agent", USER_AGENT); 98 + if let Some(key) = api_key { 99 + builder = builder.header("authorization", key); 100 + } 101 + builder 102 + } 103 + 104 + /// Create a request that creates a Hex API key. 105 + /// 106 + /// API Docs: 107 + /// 108 + /// https://github.com/hexpm/hex/blob/main/lib/mix/tasks/hex.ex#L137 109 + /// 110 + /// https://github.com/hexpm/hex/blob/main/lib/hex/api/key.ex#L6 111 + pub fn api_create_api_key_request( 112 + username: &str, 113 + password: &str, 114 + key_name: &str, 115 + config: &Config, 116 + ) -> http::Request<Vec<u8>> { 117 + let body = json!({ 118 + "name": key_name, 119 + "permissions": [{ 120 + "domain": "api", 121 + "resource": "write", 122 + }], 123 + }); 124 + let creds = http_auth_basic::Credentials::new(username, password).as_http_header(); 125 + config 126 + .api_request(Method::POST, "keys", None) 127 + .header("authorization", creds) 128 + .body(body.to_string().into_bytes()) 129 + .expect("create_api_key_request request") 130 + } 131 + 132 + /// Parses a request that creates a Hex API key. 133 + pub fn api_create_api_key_response(response: http::Response<Vec<u8>>) -> Result<String, ApiError> { 134 + #[derive(Deserialize)] 135 + struct Resp { 136 + secret: String, 137 + } 138 + let (parts, body) = response.into_parts(); 139 + match parts.status { 140 + StatusCode::CREATED => Ok(serde_json::from_slice::<Resp>(&body)?.secret), 141 + StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited), 142 + StatusCode::UNAUTHORIZED => Err(ApiError::InvalidCredentials), 143 + status => Err(ApiError::unexpected_response(status, body)), 144 + } 145 + } 146 + 147 + /// Create a request that deletes an Hex API key. 148 + /// 149 + /// API Docs: 150 + /// 151 + /// https://github.com/hexpm/hex/blob/main/lib/mix/tasks/hex.user.ex#L291 152 + /// 153 + /// https://github.com/hexpm/hex/blob/main/lib/hex/api/key.ex#L15 154 + pub fn api_remove_api_key_request( 155 + name_of_key_to_delete: &str, 156 + api_key: &str, 157 + config: &Config, 158 + ) -> http::Request<Vec<u8>> { 159 + config 160 + .api_request( 161 + Method::DELETE, 162 + &format!("keys/{}", name_of_key_to_delete), 163 + Some(api_key), 164 + ) 165 + .body(vec![]) 166 + .expect("remove_api_key_request request") 167 + } 168 + 169 + /// Parses a request that deleted a Hex API key. 170 + pub fn api_remove_api_key_response(response: http::Response<Vec<u8>>) -> Result<(), ApiError> { 171 + let (parts, body) = response.into_parts(); 172 + match parts.status { 173 + StatusCode::NO_CONTENT | StatusCode::OK => Ok(()), 174 + StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited), 175 + StatusCode::UNAUTHORIZED => Err(ApiError::InvalidCredentials), 176 + status => Err(ApiError::unexpected_response(status, body)), 177 + } 178 + } 179 + 180 + /// Retire an existing package release from Hex. 181 + /// 182 + /// API Docs: 183 + /// 184 + /// https://github.com/hexpm/hex/blob/main/lib/mix/tasks/hex.retire.ex#L75 185 + /// 186 + /// https://github.com/hexpm/hex/blob/main/lib/hex/api/release.ex#L28 187 + pub fn api_retire_release_request( 188 + package: &str, 189 + version: &str, 190 + reason: RetirementReason, 191 + message: Option<&str>, 192 + api_key: &str, 193 + config: &Config, 194 + ) -> http::Request<Vec<u8>> { 195 + let body = json!({ 196 + "reason": reason.to_str(), 197 + "message": message, 198 + }); 199 + config 200 + .api_request( 201 + Method::POST, 202 + &format!("packages/{}/releases/{}/retire", package, version), 203 + Some(api_key), 204 + ) 205 + .body(body.to_string().into_bytes()) 206 + .expect("retire_release_request request") 207 + } 208 + 209 + /// Parses a request that retired a release. 210 + pub fn api_retire_release_response(response: http::Response<Vec<u8>>) -> Result<(), ApiError> { 211 + let (parts, body) = response.into_parts(); 212 + match parts.status { 213 + StatusCode::NO_CONTENT | StatusCode::OK => Ok(()), 214 + StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited), 215 + StatusCode::UNAUTHORIZED => Err(ApiError::InvalidCredentials), 216 + status => Err(ApiError::unexpected_response(status, body)), 217 + } 218 + } 219 + 220 + /// Un-retire an existing retired package release from Hex. 221 + /// 222 + /// API Docs: 223 + /// 224 + /// https://github.com/hexpm/hex/blob/main/lib/mix/tasks/hex.retire.ex#L89 225 + /// 226 + /// https://github.com/hexpm/hex/blob/main/lib/hex/api/release.ex#L35 227 + pub fn api_unretire_release_request( 228 + package: &str, 229 + version: &str, 230 + api_key: &str, 231 + config: &Config, 232 + ) -> http::Request<Vec<u8>> { 233 + config 234 + .api_request( 235 + Method::DELETE, 236 + &format!("packages/{}/releases/{}/retire", package, version), 237 + Some(api_key), 238 + ) 239 + .body(vec![]) 240 + .expect("unretire_release_request request") 241 + } 242 + 243 + /// Parses a request that un-retired a package version. 244 + pub fn api_unretire_release_response(response: http::Response<Vec<u8>>) -> Result<(), ApiError> { 245 + let (parts, body) = response.into_parts(); 246 + match parts.status { 247 + StatusCode::NO_CONTENT | StatusCode::OK => Ok(()), 248 + StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited), 249 + StatusCode::UNAUTHORIZED => Err(ApiError::InvalidCredentials), 250 + status => Err(ApiError::unexpected_response(status, body)), 251 + } 252 + } 253 + 254 + /// Create a request that get the names and versions of all of the packages on 255 + /// the package registry. 256 + /// 257 + /// https://github.com/hexpm/specifications/blob/main/registry-v2.md 258 + /// 259 + /// TODO: Where are the API docs for this? 260 + pub fn repository_v2_get_versions_request( 261 + api_key: Option<&str>, 262 + config: &Config, 263 + ) -> http::Request<Vec<u8>> { 264 + config 265 + .repository_request(Method::GET, "versions", api_key) 266 + .header("accept", "application/json") 267 + .body(vec![]) 268 + .expect("get_repository_versions_request request") 269 + } 270 + 271 + /// Parse a request that gets the names and versions of all of the packages on 272 + /// the package registry. 273 + pub fn repository_v2_get_versions_response( 274 + response: http::Response<Vec<u8>>, 275 + public_key: &[u8], 276 + ) -> Result<HashMap<String, Vec<Version>>, ApiError> { 277 + let (parts, body) = response.into_parts(); 278 + 279 + match parts.status { 280 + StatusCode::OK => (), 281 + status => return Err(ApiError::unexpected_response(status, body)), 282 + }; 283 + 284 + let mut decoder = GzDecoder::new(body.reader()); 285 + let mut body = Vec::new(); 286 + decoder.read_to_end(&mut body)?; 287 + 288 + repository_v2_get_versions_body(&body, public_key) 289 + } 290 + 291 + /// Parse a signed binary message containing all of the packages on the package registry. 292 + pub fn repository_v2_get_versions_body( 293 + protobuf_bytes: &Vec<u8>, 294 + public_key: &[u8], 295 + ) -> Result<HashMap<String, Vec<Version>>, ApiError> { 296 + let signed = Signed::decode(protobuf_bytes.as_slice())?; 297 + 298 + let payload = 299 + verify_payload(signed, public_key).map_err(|_| ApiError::IncorrectPayloadSignature)?; 300 + 301 + let versions = Versions::decode(payload.as_slice())? 302 + .packages 303 + .into_iter() 304 + .map(|n| { 305 + let parse_version = |v: &str| { 306 + let err = |_| ApiError::InvalidVersionFormat(v.to_string()); 307 + Version::parse(v).map_err(err) 308 + }; 309 + let versions = n 310 + .versions 311 + .iter() 312 + .map(|v| parse_version(v.as_str())) 313 + .collect::<Result<Vec<Version>, ApiError>>()?; 314 + Ok((n.name, versions)) 315 + }) 316 + .collect::<Result<HashMap<_, _>, ApiError>>()?; 317 + 318 + Ok(versions) 319 + } 320 + 321 + /// Create a request to get the information for a package in the repository. 322 + /// 323 + /// https://github.com/hexpm/specifications/blob/main/registry-v2.md 324 + /// 325 + pub fn repository_v2_get_package_request( 326 + name: &str, 327 + api_key: Option<&str>, 328 + config: &Config, 329 + ) -> http::Request<Vec<u8>> { 330 + config 331 + .repository_request(Method::GET, &format!("packages/{}", name), api_key) 332 + .header("accept", "application/json") 333 + .body(vec![]) 334 + .expect("get_package_request request") 335 + } 336 + 337 + /// Parse a response to get the information for a package in the repository. 338 + /// 339 + pub fn repository_v2_get_package_response( 340 + response: http::Response<Vec<u8>>, 341 + public_key: &[u8], 342 + ) -> Result<Package, ApiError> { 343 + let (parts, body) = response.into_parts(); 344 + 345 + match parts.status { 346 + StatusCode::OK => (), 347 + StatusCode::FORBIDDEN => return Err(ApiError::NotFound), 348 + StatusCode::NOT_FOUND => return Err(ApiError::NotFound), 349 + status => { 350 + return Err(ApiError::unexpected_response(status, body)); 351 + } 352 + }; 353 + 354 + let mut decoder = GzDecoder::new(body.reader()); 355 + let mut body = Vec::new(); 356 + decoder.read_to_end(&mut body)?; 357 + 358 + repository_v2_package_parse_body(&body, public_key) 359 + } 360 + 361 + /// Parse a signed binary message containing the information for a package in the repository. 362 + pub fn repository_v2_package_parse_body( 363 + protobuf_bytes: &Vec<u8>, 364 + public_key: &[u8], 365 + ) -> Result<Package, ApiError> { 366 + let signed = Signed::decode(protobuf_bytes.as_slice())?; 367 + 368 + let payload = 369 + verify_payload(signed, public_key).map_err(|_| ApiError::IncorrectPayloadSignature)?; 370 + 371 + let package = proto::package::Package::decode(payload.as_slice())?; 372 + let releases = package 373 + .releases 374 + .clone() 375 + .into_iter() 376 + .map(proto_to_release) 377 + .collect::<Result<Vec<_>, _>>()?; 378 + let package = Package { 379 + name: package.name, 380 + repository: package.repository, 381 + releases, 382 + }; 383 + 384 + Ok(package) 385 + } 386 + 387 + /// Create a request to download a version of a package as a tarball 388 + /// TODO: Where are the API docs for this? 389 + pub fn repository_get_package_tarball_request( 390 + name: &str, 391 + version: &str, 392 + api_key: Option<&str>, 393 + config: &Config, 394 + ) -> http::Request<Vec<u8>> { 395 + config 396 + .repository_request( 397 + Method::GET, 398 + &format!("tarballs/{}-{}.tar", name, version), 399 + api_key, 400 + ) 401 + .header("accept", "application/x-tar") 402 + .body(vec![]) 403 + .expect("get_package_tarball_request request") 404 + } 405 + 406 + /// Parse a response to download a version of a package as a tarball 407 + /// 408 + pub fn repository_get_package_tarball_response( 409 + response: http::Response<Vec<u8>>, 410 + checksum: &[u8], 411 + ) -> Result<Vec<u8>, ApiError> { 412 + let (parts, body) = response.into_parts(); 413 + match parts.status { 414 + StatusCode::OK => (), 415 + StatusCode::FORBIDDEN => return Err(ApiError::NotFound), 416 + StatusCode::NOT_FOUND => return Err(ApiError::NotFound), 417 + status => { 418 + return Err(ApiError::unexpected_response(status, body)); 419 + } 420 + }; 421 + let body = read_and_check_body(body.reader(), checksum)?; 422 + Ok(body) 423 + } 424 + 425 + /// API Docs: 426 + /// 427 + /// https://github.com/hexpm/hex/blob/main/lib/mix/tasks/hex.publish.ex#L384 428 + /// 429 + /// https://github.com/hexpm/hex/blob/main/lib/hex/api/release_docs.ex#L19 430 + pub fn api_remove_docs_request( 431 + package_name: &str, 432 + version: &str, 433 + api_key: &str, 434 + config: &Config, 435 + ) -> Result<http::Request<Vec<u8>>, ApiError> { 436 + validate_package_and_version(package_name, version)?; 437 + 438 + Ok(config 439 + .api_request( 440 + Method::DELETE, 441 + &format!("packages/{}/releases/{}/docs", package_name, version), 442 + Some(api_key), 443 + ) 444 + .body(vec![]) 445 + .expect("remove_docs_request request")) 446 + } 447 + 448 + pub fn api_remove_docs_response(response: http::Response<Vec<u8>>) -> Result<(), ApiError> { 449 + let (parts, body) = response.into_parts(); 450 + match parts.status { 451 + StatusCode::NO_CONTENT => Ok(()), 452 + StatusCode::NOT_FOUND => Err(ApiError::NotFound), 453 + StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited), 454 + StatusCode::UNAUTHORIZED => Err(ApiError::InvalidApiKey), 455 + StatusCode::FORBIDDEN => Err(ApiError::Forbidden), 456 + status => Err(ApiError::unexpected_response(status, body)), 457 + } 458 + } 459 + 460 + /// API Docs: 461 + /// 462 + /// https://github.com/hexpm/hex/blob/main/lib/mix/tasks/hex.publish.ex#L429 463 + /// 464 + /// https://github.com/hexpm/hex/blob/main/lib/hex/api/release_docs.ex#L11 465 + pub fn api_publish_docs_request( 466 + package_name: &str, 467 + version: &str, 468 + gzipped_tarball: Vec<u8>, 469 + api_key: &str, 470 + config: &Config, 471 + ) -> Result<http::Request<Vec<u8>>, ApiError> { 472 + validate_package_and_version(package_name, version)?; 473 + 474 + Ok(config 475 + .api_request( 476 + Method::POST, 477 + &format!("packages/{}/releases/{}/docs", package_name, version), 478 + Some(api_key), 479 + ) 480 + .header("content-encoding", "x-gzip") 481 + .header("content-type", "application/x-tar") 482 + .body(gzipped_tarball) 483 + .expect("publish_docs_request request")) 484 + } 485 + 486 + pub fn api_publish_docs_response(response: http::Response<Vec<u8>>) -> Result<(), ApiError> { 487 + let (parts, body) = response.into_parts(); 488 + match parts.status { 489 + StatusCode::CREATED => Ok(()), 490 + StatusCode::NOT_FOUND => Err(ApiError::NotFound), 491 + StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited), 492 + StatusCode::UNAUTHORIZED => Err(ApiError::InvalidApiKey), 493 + StatusCode::FORBIDDEN => Err(ApiError::Forbidden), 494 + status => Err(ApiError::unexpected_response(status, body)), 495 + } 496 + } 497 + 498 + /// API Docs: 499 + /// 500 + /// https://github.com/hexpm/hex/blob/main/lib/mix/tasks/hex.publish.ex#L512 501 + /// 502 + /// https://github.com/hexpm/hex/blob/main/lib/hex/api/release.ex#L13 503 + pub fn api_publish_package_request( 504 + release_tarball: Vec<u8>, 505 + api_key: &str, 506 + config: &Config, 507 + replace: bool, 508 + ) -> http::Request<Vec<u8>> { 509 + // TODO: do all the package tarball construction 510 + config 511 + .api_request( 512 + Method::POST, 513 + format!("publish?replace={}", replace).as_str(), 514 + Some(api_key), 515 + ) 516 + .header("content-type", "application/x-tar") 517 + .body(release_tarball) 518 + .expect("publish_package_request request") 519 + } 520 + 521 + pub fn api_publish_package_response(response: http::Response<Vec<u8>>) -> Result<(), ApiError> { 522 + // TODO: return data from body 523 + let (parts, body) = response.into_parts(); 524 + match parts.status { 525 + StatusCode::OK | StatusCode::CREATED => Ok(()), 526 + StatusCode::NOT_FOUND => Err(ApiError::NotFound), 527 + StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited), 528 + StatusCode::UNAUTHORIZED => Err(ApiError::InvalidApiKey), 529 + StatusCode::FORBIDDEN => Err(ApiError::Forbidden), 530 + StatusCode::UNPROCESSABLE_ENTITY => { 531 + let body = &String::from_utf8_lossy(&body).to_string(); 532 + if body.contains("--replace") { 533 + return Err(ApiError::NotReplacing); 534 + } 535 + Err(ApiError::LateModification) 536 + } 537 + status => Err(ApiError::unexpected_response(status, body)), 538 + } 539 + } 540 + 541 + /// API Docs: 542 + /// 543 + /// https://github.com/hexpm/hex/blob/main/lib/mix/tasks/hex.publish.ex#L371 544 + /// 545 + /// https://github.com/hexpm/hex/blob/main/lib/hex/api/release.ex#L21 546 + pub fn api_revert_release_request( 547 + package_name: &str, 548 + version: &str, 549 + api_key: &str, 550 + config: &Config, 551 + ) -> Result<http::Request<Vec<u8>>, ApiError> { 552 + validate_package_and_version(package_name, version)?; 553 + 554 + Ok(config 555 + .api_request( 556 + Method::DELETE, 557 + &format!("packages/{}/releases/{}", package_name, version), 558 + Some(api_key), 559 + ) 560 + .body(vec![]) 561 + .expect("publish_package_request request")) 562 + } 563 + 564 + pub fn api_revert_release_response(response: http::Response<Vec<u8>>) -> Result<(), ApiError> { 565 + let (parts, body) = response.into_parts(); 566 + match parts.status { 567 + StatusCode::NO_CONTENT => Ok(()), 568 + StatusCode::NOT_FOUND => Err(ApiError::NotFound), 569 + StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited), 570 + StatusCode::UNAUTHORIZED => Err(ApiError::InvalidApiKey), 571 + StatusCode::FORBIDDEN => Err(ApiError::Forbidden), 572 + status => Err(ApiError::unexpected_response(status, body)), 573 + } 574 + } 575 + 576 + /// See: https://github.com/hexpm/hex/blob/main/lib/mix/tasks/hex.owner.ex#L47 577 + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 578 + pub enum OwnerLevel { 579 + /// Has every package permission EXCEPT the ability to change who owns the package 580 + Maintainer, 581 + /// Has every package permission including the ability to change who owns the package 582 + Full, 583 + } 584 + 585 + impl Display for OwnerLevel { 586 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 587 + match self { 588 + OwnerLevel::Maintainer => write!(f, "maintainer"), 589 + OwnerLevel::Full => write!(f, "full"), 590 + } 591 + } 592 + } 593 + 594 + /// API Docs: 595 + /// 596 + /// https://github.com/hexpm/hex/blob/main/lib/mix/tasks/hex.owner.ex#L107 597 + /// 598 + /// https://github.com/hexpm/hex/blob/main/lib/hex/api/package.ex#L19 599 + pub fn api_add_owner_request( 600 + package_name: &str, 601 + owner: &str, 602 + level: OwnerLevel, 603 + api_key: &str, 604 + config: &Config, 605 + ) -> http::Request<Vec<u8>> { 606 + let body = json!({ 607 + "level": level.to_string(), 608 + "transfer": false, 609 + }); 610 + 611 + config 612 + .api_request( 613 + Method::PUT, 614 + &format!("packages/{}/owners/{}", package_name, owner), 615 + Some(api_key), 616 + ) 617 + .body(body.to_string().into_bytes()) 618 + .expect("add_owner_request request") 619 + } 620 + 621 + pub fn api_add_owner_response(response: http::Response<Vec<u8>>) -> Result<(), ApiError> { 622 + let (parts, body) = response.into_parts(); 623 + match parts.status { 624 + StatusCode::NO_CONTENT => Ok(()), 625 + StatusCode::NOT_FOUND => Err(ApiError::NotFound), 626 + StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited), 627 + StatusCode::UNAUTHORIZED => Err(ApiError::InvalidApiKey), 628 + StatusCode::FORBIDDEN => Err(ApiError::Forbidden), 629 + status => Err(ApiError::unexpected_response(status, body)), 630 + } 631 + } 632 + 633 + /// API Docs: 634 + /// 635 + /// https://github.com/hexpm/hex/blob/main/lib/mix/tasks/hex.owner.ex#L125 636 + /// 637 + /// https://github.com/hexpm/hex/blob/main/lib/hex/api/package.ex#L19 638 + pub fn api_transfer_owner_request( 639 + package_name: &str, 640 + owner: &str, 641 + api_key: &str, 642 + config: &Config, 643 + ) -> http::Request<Vec<u8>> { 644 + let body = json!({ 645 + "level": OwnerLevel::Full.to_string(), 646 + "transfer": true, 647 + }); 648 + 649 + config 650 + .api_request( 651 + Method::PUT, 652 + &format!("packages/{}/owners/{}", package_name, owner), 653 + Some(api_key), 654 + ) 655 + .body(body.to_string().into_bytes()) 656 + .expect("transfer_owner_request request") 657 + } 658 + 659 + pub fn api_transfer_owner_response(response: http::Response<Vec<u8>>) -> Result<(), ApiError> { 660 + let (parts, body) = response.into_parts(); 661 + match parts.status { 662 + StatusCode::NO_CONTENT => Ok(()), 663 + StatusCode::NOT_FOUND => Err(ApiError::NotFound), 664 + StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited), 665 + StatusCode::UNAUTHORIZED => Err(ApiError::InvalidApiKey), 666 + StatusCode::FORBIDDEN => Err(ApiError::Forbidden), 667 + status => Err(ApiError::unexpected_response(status, body)), 668 + } 669 + } 670 + 671 + /// API Docs: 672 + /// 673 + /// https://github.com/hexpm/hex/blob/main/lib/mix/tasks/hex.owner.ex#L139 674 + /// 675 + /// https://github.com/hexpm/hex/blob/main/lib/hex/api/package.ex#L28 676 + pub fn api_remove_owner_request( 677 + package_name: &str, 678 + owner: &str, 679 + api_key: &str, 680 + config: &Config, 681 + ) -> http::Request<Vec<u8>> { 682 + config 683 + .api_request( 684 + Method::DELETE, 685 + &format!("packages/{}/owners/{}", package_name, owner), 686 + Some(api_key), 687 + ) 688 + .body(vec![]) 689 + .expect("remove_owner_request request") 690 + } 691 + 692 + pub fn api_remove_owner_response(response: http::Response<Vec<u8>>) -> Result<(), ApiError> { 693 + let (parts, body) = response.into_parts(); 694 + match parts.status { 695 + StatusCode::NO_CONTENT => Ok(()), 696 + StatusCode::NOT_FOUND => Err(ApiError::NotFound), 697 + StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited), 698 + StatusCode::UNAUTHORIZED => Err(ApiError::InvalidApiKey), 699 + StatusCode::FORBIDDEN => Err(ApiError::Forbidden), 700 + status => Err(ApiError::unexpected_response(status, body)), 701 + } 702 + } 703 + 704 + #[derive(Error, Debug)] 705 + pub enum ApiError { 706 + #[error(transparent)] 707 + Json(#[from] serde_json::Error), 708 + 709 + #[error(transparent)] 710 + Io(#[from] std::io::Error), 711 + 712 + #[error("the rate limit for the Hex API has been exceeded for this IP")] 713 + RateLimited, 714 + 715 + #[error("invalid username and password combination")] 716 + InvalidCredentials, 717 + 718 + #[error("an unexpected response was sent by Hex: {0}: {1}")] 719 + UnexpectedResponse(StatusCode, String), 720 + 721 + #[error("the given package name {0} is not valid")] 722 + InvalidPackageNameFormat(String), 723 + 724 + #[error("the payload signature does not match the downloaded payload")] 725 + IncorrectPayloadSignature, 726 + 727 + #[error(transparent)] 728 + InvalidProtobuf(#[from] prost::DecodeError), 729 + 730 + #[error("unexpected version format {0}")] 731 + InvalidVersionFormat(String), 732 + 733 + #[error("resource was not found")] 734 + NotFound, 735 + 736 + #[error("the version requirement format {0} is not valid")] 737 + InvalidVersionRequirementFormat(String), 738 + 739 + #[error("the downloaded data did not have the expected checksum")] 740 + IncorrectChecksum, 741 + 742 + #[error("the given API key was not valid")] 743 + InvalidApiKey, 744 + 745 + #[error("this account is not authorized for this action")] 746 + Forbidden, 747 + 748 + #[error("must explicitly express your intention to replace the release")] 749 + NotReplacing, 750 + 751 + #[error("can only modify a release up to one hour after publication")] 752 + LateModification, 753 + } 754 + 755 + impl ApiError { 756 + fn unexpected_response(status: StatusCode, body: Vec<u8>) -> Self { 757 + ApiError::UnexpectedResponse(status, String::from_utf8_lossy(&body).to_string()) 758 + } 759 + 760 + /// Returns `true` if the api error is [`NotFound`]. 761 + /// 762 + /// [`NotFound`]: ApiError::NotFound 763 + pub fn is_not_found(&self) -> bool { 764 + matches!(self, Self::NotFound) 765 + } 766 + 767 + pub fn is_invalid_protobuf(&self) -> bool { 768 + matches!(self, Self::InvalidProtobuf(_)) 769 + } 770 + } 771 + 772 + /// Read a body and ensure it has the given sha256 digest. 773 + fn read_and_check_body(reader: impl std::io::Read, checksum: &[u8]) -> Result<Vec<u8>, ApiError> { 774 + use std::io::Read; 775 + let mut reader = BufReader::new(reader); 776 + let mut context = Context::new(&SHA256); 777 + let mut buffer = [0; 1024]; 778 + let mut body = Vec::new(); 779 + 780 + loop { 781 + let count = reader.read(&mut buffer)?; 782 + if count == 0 { 783 + break; 784 + } 785 + let bytes = &buffer[..count]; 786 + context.update(bytes); 787 + body.extend_from_slice(bytes); 788 + } 789 + 790 + let digest = context.finish(); 791 + if digest.as_ref() == checksum { 792 + Ok(body) 793 + } else { 794 + Err(ApiError::IncorrectChecksum) 795 + } 796 + } 797 + 798 + fn proto_to_retirement_status( 799 + status: Option<proto::package::RetirementStatus>, 800 + ) -> Option<RetirementStatus> { 801 + status.map(|stat| RetirementStatus { 802 + message: stat.message().into(), 803 + reason: proto_to_retirement_reason(stat.reason()), 804 + }) 805 + } 806 + 807 + fn proto_to_retirement_reason(reason: proto::package::RetirementReason) -> RetirementReason { 808 + use proto::package::RetirementReason::*; 809 + match reason { 810 + RetiredOther => RetirementReason::Other, 811 + RetiredInvalid => RetirementReason::Invalid, 812 + RetiredSecurity => RetirementReason::Security, 813 + RetiredDeprecated => RetirementReason::Deprecated, 814 + RetiredRenamed => RetirementReason::Renamed, 815 + } 816 + } 817 + 818 + fn proto_to_dep(dep: proto::package::Dependency) -> Result<(String, Dependency), ApiError> { 819 + let app = dep.app; 820 + let repository = dep.repository; 821 + let requirement = Range::new(dep.requirement.clone()) 822 + .map_err(|_| ApiError::InvalidVersionFormat(dep.requirement))?; 823 + Ok(( 824 + dep.package, 825 + Dependency { 826 + requirement, 827 + optional: dep.optional.is_some(), 828 + app, 829 + repository, 830 + }, 831 + )) 832 + } 833 + 834 + fn proto_to_release(release: proto::package::Release) -> Result<Release<()>, ApiError> { 835 + let dependencies = release 836 + .dependencies 837 + .clone() 838 + .into_iter() 839 + .map(proto_to_dep) 840 + .collect::<Result<HashMap<_, _>, _>>()?; 841 + let version = Version::try_from(release.version.as_str()) 842 + .expect("Failed to parse version format from Hex"); 843 + Ok(Release { 844 + version, 845 + outer_checksum: release.outer_checksum.unwrap_or_default(), 846 + retirement_status: proto_to_retirement_status(release.retired), 847 + requirements: dependencies, 848 + meta: (), 849 + }) 850 + } 851 + 852 + #[derive(Debug, PartialEq, Eq, Clone)] 853 + pub struct Package { 854 + pub name: String, 855 + pub repository: String, 856 + pub releases: Vec<Release<()>>, 857 + } 858 + 859 + #[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize)] 860 + pub struct Release<Meta> { 861 + /// Release version 862 + pub version: Version, 863 + /// All dependencies of the release 864 + pub requirements: HashMap<String, Dependency>, 865 + /// If set the release is retired, a retired release should only be 866 + /// resolved if it has already been locked in a project 867 + pub retirement_status: Option<RetirementStatus>, 868 + /// sha256 checksum of outer package tarball 869 + /// required when encoding but optional when decoding 870 + #[serde(alias = "checksum", deserialize_with = "deserialize_checksum")] 871 + pub outer_checksum: Vec<u8>, 872 + /// This is not present in all API endpoints so may be absent sometimes. 873 + pub meta: Meta, 874 + } 875 + 876 + fn deserialize_checksum<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error> 877 + where 878 + D: serde::Deserializer<'de>, 879 + { 880 + let s: &str = serde::de::Deserialize::deserialize(deserializer)?; 881 + base16::decode(s).map_err(serde::de::Error::custom) 882 + } 883 + 884 + impl<Meta> Release<Meta> { 885 + pub fn is_retired(&self) -> bool { 886 + self.retirement_status.is_some() 887 + } 888 + } 889 + 890 + #[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize)] 891 + pub struct ReleaseMeta { 892 + pub app: String, 893 + pub build_tools: Vec<String>, 894 + } 895 + 896 + #[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize)] 897 + pub struct RetirementStatus { 898 + pub reason: RetirementReason, 899 + pub message: String, 900 + } 901 + 902 + #[derive(Debug, PartialEq, Eq, Clone)] 903 + pub enum RetirementReason { 904 + Other, 905 + Invalid, 906 + Security, 907 + Deprecated, 908 + Renamed, 909 + } 910 + 911 + impl<'de> serde::Deserialize<'de> for RetirementReason { 912 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 913 + where 914 + D: serde::Deserializer<'de>, 915 + { 916 + let s: &str = serde::de::Deserialize::deserialize(deserializer)?; 917 + match s { 918 + "other" => Ok(RetirementReason::Other), 919 + "invalid" => Ok(RetirementReason::Invalid), 920 + "security" => Ok(RetirementReason::Security), 921 + "deprecated" => Ok(RetirementReason::Deprecated), 922 + "renamed" => Ok(RetirementReason::Renamed), 923 + _ => Err(serde::de::Error::custom("unknown retirement reason type")), 924 + } 925 + } 926 + } 927 + 928 + impl RetirementReason { 929 + pub fn to_str(&self) -> &'static str { 930 + match self { 931 + RetirementReason::Other => "other", 932 + RetirementReason::Invalid => "invalid", 933 + RetirementReason::Security => "security", 934 + RetirementReason::Deprecated => "deprecated", 935 + RetirementReason::Renamed => "renamed", 936 + } 937 + } 938 + } 939 + 940 + #[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize)] 941 + pub struct Dependency { 942 + /// Version requirement of dependency 943 + pub requirement: Range, 944 + /// If true the package is optional and does not need to be resolved 945 + /// unless another package has specified it as a non-optional dependency. 946 + pub optional: bool, 947 + /// If set is the OTP application name of the dependency, if not set the 948 + /// application name is the same as the package name 949 + pub app: Option<String>, 950 + /// If set, the repository where the dependency is located 951 + pub repository: Option<String>, 952 + } 953 + 954 + static USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), " (", env!("CARGO_PKG_VERSION"), ")"); 955 + 956 + fn validate_package_and_version(package: &str, version: &str) -> Result<(), ApiError> { 957 + lazy_static! { 958 + static ref PACKAGE_PATTERN: Regex = Regex::new(r"^[a-z]\w*$").unwrap(); 959 + static ref VERSION_PATTERN: Regex = Regex::new(r"^[a-zA-Z-0-9\._-]+$").unwrap(); 960 + } 961 + if !PACKAGE_PATTERN.is_match(package) { 962 + return Err(ApiError::InvalidPackageNameFormat(package.to_string())); 963 + } 964 + if !VERSION_PATTERN.is_match(version) { 965 + return Err(ApiError::InvalidVersionFormat(version.to_string())); 966 + } 967 + Ok(()) 968 + } 969 + 970 + // To quote the docs: 971 + // 972 + // > All resources will be signed by the repository's private key. 973 + // > A signed resource is wrapped in a Signed message. The data under 974 + // > the payload field is signed by the signature field. 975 + // > 976 + // > The signature is an (unencoded) RSA signature of the (unencoded) 977 + // > SHA-512 digest of the payload. 978 + // 979 + // https://github.com/hexpm/specifications/blob/master/registry-v2.md#signing 980 + // 981 + fn verify_payload(mut signed: Signed, pem_public_key: &[u8]) -> Result<Vec<u8>, ApiError> { 982 + let (_, pem) = x509_parser::pem::parse_x509_pem(pem_public_key) 983 + .map_err(|_| ApiError::IncorrectPayloadSignature)?; 984 + let (_, spki) = x509_parser::prelude::SubjectPublicKeyInfo::from_der(&pem.contents) 985 + .map_err(|_| ApiError::IncorrectPayloadSignature)?; 986 + let payload = std::mem::take(&mut signed.payload); 987 + let verification = ring::signature::UnparsedPublicKey::new( 988 + &ring::signature::RSA_PKCS1_2048_8192_SHA512, 989 + &spki.subject_public_key, 990 + ) 991 + .verify(payload.as_slice(), signed.signature()); 992 + 993 + if verification.is_ok() { 994 + Ok(payload) 995 + } else { 996 + Err(ApiError::IncorrectPayloadSignature) 997 + } 998 + } 999 + 1000 + /// Create a request to get the information for a package release. 1001 + /// 1002 + pub fn api_get_package_release_request( 1003 + name: &str, 1004 + version: &str, 1005 + api_key: Option<&str>, 1006 + config: &Config, 1007 + ) -> http::Request<Vec<u8>> { 1008 + config 1009 + .api_request( 1010 + Method::GET, 1011 + &format!("packages/{}/releases/{}", name, version), 1012 + api_key, 1013 + ) 1014 + .header("accept", "application/json") 1015 + .body(vec![]) 1016 + .expect("get_package_release request") 1017 + } 1018 + 1019 + /// Parse a response to get the information for a package release. 1020 + /// 1021 + pub fn api_get_package_release_response( 1022 + response: http::Response<Vec<u8>>, 1023 + ) -> Result<Release<ReleaseMeta>, ApiError> { 1024 + let (parts, body) = response.into_parts(); 1025 + 1026 + match parts.status { 1027 + StatusCode::OK => Ok(serde_json::from_slice(&body)?), 1028 + StatusCode::NOT_FOUND => Err(ApiError::NotFound), 1029 + StatusCode::TOO_MANY_REQUESTS => Err(ApiError::RateLimited), 1030 + StatusCode::UNAUTHORIZED => Err(ApiError::InvalidApiKey), 1031 + StatusCode::FORBIDDEN => Err(ApiError::Forbidden), 1032 + status => Err(ApiError::unexpected_response(status, body)), 1033 + } 1034 + }
+5
hexpm/src/proto.rs
··· 1 + #![allow(clippy::enum_variant_names)] 2 + 3 + pub mod package; 4 + pub mod signed; 5 + pub mod versions;
+95
hexpm/src/proto/package.rs
··· 1 + // This file is @generated by prost-build. 2 + #[derive(Clone, PartialEq, ::prost::Message)] 3 + pub struct Package { 4 + /// All releases of the package 5 + #[prost(message, repeated, tag = "1")] 6 + pub releases: ::prost::alloc::vec::Vec<Release>, 7 + /// Name of package 8 + #[prost(string, required, tag = "2")] 9 + pub name: ::prost::alloc::string::String, 10 + /// Name of repository 11 + #[prost(string, required, tag = "3")] 12 + pub repository: ::prost::alloc::string::String, 13 + } 14 + #[derive(Clone, PartialEq, ::prost::Message)] 15 + pub struct Release { 16 + /// Release version 17 + #[prost(string, required, tag = "1")] 18 + pub version: ::prost::alloc::string::String, 19 + /// sha256 checksum of "inner" package tarball 20 + /// deprecated in favor of outer_checksum 21 + #[prost(bytes = "vec", required, tag = "2")] 22 + pub inner_checksum: ::prost::alloc::vec::Vec<u8>, 23 + /// All dependencies of the release 24 + #[prost(message, repeated, tag = "3")] 25 + pub dependencies: ::prost::alloc::vec::Vec<Dependency>, 26 + /// If set the release is retired, a retired release should only be 27 + /// resolved if it has already been locked in a project 28 + #[prost(message, optional, tag = "4")] 29 + pub retired: ::core::option::Option<RetirementStatus>, 30 + /// sha256 checksum of outer package tarball 31 + /// required when encoding but optional when decoding 32 + #[prost(bytes = "vec", optional, tag = "5")] 33 + pub outer_checksum: ::core::option::Option<::prost::alloc::vec::Vec<u8>>, 34 + } 35 + #[derive(Clone, PartialEq, ::prost::Message)] 36 + pub struct RetirementStatus { 37 + #[prost(enumeration = "RetirementReason", required, tag = "1")] 38 + pub reason: i32, 39 + #[prost(string, optional, tag = "2")] 40 + pub message: ::core::option::Option<::prost::alloc::string::String>, 41 + } 42 + #[derive(Clone, PartialEq, ::prost::Message)] 43 + pub struct Dependency { 44 + /// Package name of dependency 45 + #[prost(string, required, tag = "1")] 46 + pub package: ::prost::alloc::string::String, 47 + /// Version requirement of dependency 48 + #[prost(string, required, tag = "2")] 49 + pub requirement: ::prost::alloc::string::String, 50 + /// If set and true the package is optional (see dependency resolution) 51 + #[prost(bool, optional, tag = "3")] 52 + pub optional: ::core::option::Option<bool>, 53 + /// If set is the OTP application name of the dependency, if not set the 54 + /// application name is the same as the package name 55 + #[prost(string, optional, tag = "4")] 56 + pub app: ::core::option::Option<::prost::alloc::string::String>, 57 + /// If set, the repository where the dependency is located 58 + #[prost(string, optional, tag = "5")] 59 + pub repository: ::core::option::Option<::prost::alloc::string::String>, 60 + } 61 + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] 62 + #[repr(i32)] 63 + pub enum RetirementReason { 64 + RetiredOther = 0, 65 + RetiredInvalid = 1, 66 + RetiredSecurity = 2, 67 + RetiredDeprecated = 3, 68 + RetiredRenamed = 4, 69 + } 70 + impl RetirementReason { 71 + /// String value of the enum field names used in the ProtoBuf definition. 72 + /// 73 + /// The values are not transformed in any way and thus are considered stable 74 + /// (if the ProtoBuf definition does not change) and safe for programmatic use. 75 + pub fn as_str_name(&self) -> &'static str { 76 + match self { 77 + Self::RetiredOther => "RETIRED_OTHER", 78 + Self::RetiredInvalid => "RETIRED_INVALID", 79 + Self::RetiredSecurity => "RETIRED_SECURITY", 80 + Self::RetiredDeprecated => "RETIRED_DEPRECATED", 81 + Self::RetiredRenamed => "RETIRED_RENAMED", 82 + } 83 + } 84 + /// Creates an enum from field names used in the ProtoBuf definition. 85 + pub fn from_str_name(value: &str) -> ::core::option::Option<Self> { 86 + match value { 87 + "RETIRED_OTHER" => Some(Self::RetiredOther), 88 + "RETIRED_INVALID" => Some(Self::RetiredInvalid), 89 + "RETIRED_SECURITY" => Some(Self::RetiredSecurity), 90 + "RETIRED_DEPRECATED" => Some(Self::RetiredDeprecated), 91 + "RETIRED_RENAMED" => Some(Self::RetiredRenamed), 92 + _ => None, 93 + } 94 + } 95 + }
+10
hexpm/src/proto/signed.rs
··· 1 + // This file is @generated by prost-build. 2 + #[derive(Clone, PartialEq, ::prost::Message)] 3 + pub struct Signed { 4 + /// Signed contents 5 + #[prost(bytes = "vec", required, tag = "1")] 6 + pub payload: ::prost::alloc::vec::Vec<u8>, 7 + /// The signature 8 + #[prost(bytes = "vec", optional, tag = "2")] 9 + pub signature: ::core::option::Option<::prost::alloc::vec::Vec<u8>>, 10 + }
+25
hexpm/src/proto/versions.rs
··· 1 + // This file is @generated by prost-build. 2 + #[derive(Clone, PartialEq, ::prost::Message)] 3 + pub struct Versions { 4 + /// All packages in the repository 5 + #[prost(message, repeated, tag = "1")] 6 + pub packages: ::prost::alloc::vec::Vec<VersionsPackage>, 7 + /// Name of repository 8 + #[prost(string, required, tag = "2")] 9 + pub repository: ::prost::alloc::string::String, 10 + } 11 + #[derive(Clone, PartialEq, ::prost::Message)] 12 + pub struct VersionsPackage { 13 + /// Package name 14 + #[prost(string, required, tag = "1")] 15 + pub name: ::prost::alloc::string::String, 16 + /// All released versions of the package 17 + #[prost(string, repeated, tag = "2")] 18 + pub versions: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, 19 + /// Zero-based indexes of retired versions in the versions field, see package.proto 20 + /// 21 + /// If set, the name of the package repository (NEVER USED, DEPRECATED) 22 + /// string repository = 4; 23 + #[prost(int32, repeated, tag = "3")] 24 + pub retired: ::prost::alloc::vec::Vec<i32>, 25 + }
+1358
hexpm/src/tests.rs
··· 1 + // TODO: remove all the async stuff and mockito server. The library is pure now 2 + // so it isn't needed. 3 + 4 + use std::{convert::TryFrom, io::Cursor}; 5 + 6 + use super::*; 7 + use mockito::Matcher; 8 + use serde_json::json; 9 + 10 + async fn http_send<Body: Into<reqwest::Body>>( 11 + request: http::Request<Body>, 12 + ) -> Result<http::Response<Vec<u8>>, reqwest::Error> { 13 + // Make the request 14 + let mut response = reqwest::Client::new() 15 + .execute(request.try_into().unwrap()) 16 + .await?; 17 + // Convert to http::Response 18 + let mut builder = http::Response::builder() 19 + .status(response.status()) 20 + .version(response.version()); 21 + std::mem::swap(builder.headers_mut().unwrap(), response.headers_mut()); 22 + Ok(builder 23 + .body(response.bytes().await.unwrap().to_vec()) 24 + .unwrap()) 25 + } 26 + 27 + #[tokio::test] 28 + async fn authenticate_test_success() { 29 + let username = "me@example.com"; 30 + let password = "password"; 31 + let name = "louis-test"; 32 + let expected_secret = "some-secret-here"; 33 + 34 + let resp_body = json!({ 35 + "authing_key": false, 36 + "inserted_at": "2020-05-02T17:18:23.336328Z", 37 + "name": "authenticate_test_1", 38 + "permissions": [{"domain": "api", "resource": "write"}], 39 + "revoked_at": null, 40 + "secret": expected_secret, 41 + "updated_at": "2020-05-02T17: 18: 23.336328Z", 42 + "url": "https: //hex.pm/api/keys/authenticate_test_1" 43 + }); 44 + 45 + let mut server = mockito::Server::new_async().await; 46 + let mock = server 47 + .mock("POST", "/keys") 48 + .expect(1) 49 + .match_header("authorization", "Basic bWVAZXhhbXBsZS5jb206cGFzc3dvcmQ=") 50 + .match_header("content-type", "application/json") 51 + .match_header("accept", "application/json") 52 + .match_body(Matcher::Json(json!({ 53 + "name": name, 54 + "permissions":[{ "domain": "api", "resource": "write" }] 55 + }))) 56 + .with_status(201) 57 + .with_body(resp_body.to_string()) 58 + .create_async() 59 + .await; 60 + 61 + let mut config = Config::new(); 62 + config.api_base = http::Uri::try_from(server.url()).unwrap(); 63 + 64 + let secret = crate::api_create_api_key_response( 65 + http_send(crate::api_create_api_key_request( 66 + username, password, name, &config, 67 + )) 68 + .await 69 + .unwrap(), 70 + ) 71 + .unwrap(); 72 + 73 + assert_eq!(expected_secret, secret); 74 + mock.assert(); 75 + } 76 + 77 + #[tokio::test] 78 + async fn authenticate_test_rate_limted() { 79 + let username = "me@example.com"; 80 + let password = "password"; 81 + let name = "authenticate_test_2"; 82 + 83 + let mut server = mockito::Server::new_async().await; 84 + let mock = server 85 + .mock("POST", "/keys") 86 + .expect(1) 87 + .match_header("authorization", "Basic bWVAZXhhbXBsZS5jb206cGFzc3dvcmQ=") 88 + .match_header("content-type", "application/json") 89 + .match_header("accept", "application/json") 90 + .match_body(Matcher::Json(json!({ 91 + "name": name, 92 + "permissions":[{ "domain": "api", "resource": "write" }] 93 + }))) 94 + .with_status(429) 95 + .create_async() 96 + .await; 97 + 98 + let mut config = Config::new(); 99 + config.api_base = http::Uri::try_from(server.url()).unwrap(); 100 + 101 + let result = crate::api_create_api_key_response( 102 + http_send(crate::api_create_api_key_request( 103 + username, password, name, &config, 104 + )) 105 + .await 106 + .unwrap(), 107 + ) 108 + .unwrap_err(); 109 + 110 + match result { 111 + ApiError::RateLimited => (), 112 + result => panic!("expected RateLimited, got {:?}", result), 113 + } 114 + 115 + mock.assert(); 116 + } 117 + 118 + #[tokio::test] 119 + async fn authenticate_test_bad_creds() { 120 + let username = "me@example.com"; 121 + let password = "password"; 122 + let name = "authenticate_test_3"; 123 + 124 + let resp_body = json!({ 125 + "message": "invalid username and password combination", 126 + "status": 401, 127 + }); 128 + 129 + let mut server = mockito::Server::new_async().await; 130 + let mock = server 131 + .mock("POST", "/keys") 132 + .expect(1) 133 + .match_header("authorization", "Basic bWVAZXhhbXBsZS5jb206cGFzc3dvcmQ=") 134 + .match_header("content-type", "application/json") 135 + .match_header("accept", "application/json") 136 + .match_body(Matcher::Json(json!({ 137 + "name": name, 138 + "permissions":[{ "domain": "api", "resource": "write" }] 139 + }))) 140 + .with_status(401) 141 + .with_body(resp_body.to_string()) 142 + .create_async() 143 + .await; 144 + 145 + let mut config = Config::new(); 146 + config.api_base = http::Uri::try_from(server.url()).unwrap(); 147 + 148 + let result = crate::api_create_api_key_response( 149 + http_send(crate::api_create_api_key_request( 150 + username, password, name, &config, 151 + )) 152 + .await 153 + .unwrap(), 154 + ) 155 + .unwrap_err(); 156 + 157 + match result { 158 + ApiError::InvalidCredentials => (), 159 + result => panic!("expected InvalidCredentials, got {:?}", result), 160 + } 161 + 162 + mock.assert(); 163 + } 164 + 165 + #[tokio::test] 166 + async fn remove_docs_success() { 167 + let key = "my-api-key-here"; 168 + let package = "gleam_experimental_stdlib"; 169 + let version = "0.8.0"; 170 + 171 + let mut server = mockito::Server::new_async().await; 172 + let mock = server 173 + .mock( 174 + "DELETE", 175 + format!("/packages/{}/releases/{}/docs", package, version).as_str(), 176 + ) 177 + .expect(1) 178 + .match_header("authorization", key) 179 + .match_header("accept", "application/json") 180 + .with_status(204) 181 + .create_async() 182 + .await; 183 + 184 + let mut config = Config::new(); 185 + config.api_base = http::Uri::try_from(server.url()).unwrap(); 186 + 187 + let result = crate::api_remove_docs_response( 188 + http_send(crate::api_remove_docs_request(package, version, key, &config).unwrap()) 189 + .await 190 + .unwrap(), 191 + ) 192 + .unwrap(); 193 + 194 + assert_eq!(result, ()); 195 + mock.assert(); 196 + } 197 + 198 + #[tokio::test] 199 + async fn revert_release_success() { 200 + let key = "my-api-key-here"; 201 + let package = "gleam_experimental_stdlib"; 202 + let version = "0.8.0"; 203 + 204 + let mut server = mockito::Server::new_async().await; 205 + let mock = server 206 + .mock( 207 + "DELETE", 208 + format!("/packages/{}/releases/{}", package, version).as_str(), 209 + ) 210 + .expect(1) 211 + .match_header("authorization", key) 212 + .match_header("accept", "application/json") 213 + .with_status(204) 214 + .create_async() 215 + .await; 216 + 217 + let mut config = Config::new(); 218 + config.api_base = http::Uri::try_from(server.url()).unwrap(); 219 + 220 + let result = crate::api_revert_release_response( 221 + http_send(crate::api_revert_release_request(package, version, key, &config).unwrap()) 222 + .await 223 + .unwrap(), 224 + ) 225 + .unwrap(); 226 + 227 + assert_eq!(result, ()); 228 + mock.assert(); 229 + } 230 + 231 + #[tokio::test] 232 + async fn add_owner_success() { 233 + let key = "my-api-key-here"; 234 + let package = "gleam_experimental_stdlib"; 235 + let owner = "lpil"; 236 + let level = OwnerLevel::Maintainer; 237 + 238 + let mut server = mockito::Server::new_async().await; 239 + let mock = server 240 + .mock( 241 + "PUT", 242 + format!("/packages/{}/owners/{}", package, owner).as_str(), 243 + ) 244 + .expect(1) 245 + .match_header("authorization", key) 246 + .match_header("accept", "application/json") 247 + .match_body(Matcher::Json(json!({ 248 + "level": "maintainer", 249 + "transfer": false, 250 + }))) 251 + .with_status(204) 252 + .create_async() 253 + .await; 254 + 255 + let mut config = Config::new(); 256 + config.api_base = http::Uri::try_from(server.url()).unwrap(); 257 + 258 + let result = crate::api_add_owner_response( 259 + http_send(crate::api_add_owner_request( 260 + package, owner, level, key, &config, 261 + )) 262 + .await 263 + .unwrap(), 264 + ) 265 + .unwrap(); 266 + 267 + assert_eq!(result, ()); 268 + mock.assert(); 269 + } 270 + 271 + #[tokio::test] 272 + async fn transfer_owner_success() { 273 + let key = "my-api-key-here"; 274 + let package = "gleam_experimental_stdlib"; 275 + let owner = "lpil"; 276 + 277 + let mut server = mockito::Server::new_async().await; 278 + let mock = server 279 + .mock( 280 + "PUT", 281 + format!("/packages/{}/owners/{}", package, owner).as_str(), 282 + ) 283 + .expect(1) 284 + .match_header("authorization", key) 285 + .match_header("accept", "application/json") 286 + .match_body(Matcher::Json(json!({ 287 + "level": "full", 288 + "transfer": true, 289 + }))) 290 + .with_status(204) 291 + .create_async() 292 + .await; 293 + 294 + let mut config = Config::new(); 295 + config.api_base = http::Uri::try_from(server.url()).unwrap(); 296 + 297 + let result = crate::api_transfer_owner_response( 298 + http_send(crate::api_transfer_owner_request( 299 + package, owner, key, &config, 300 + )) 301 + .await 302 + .unwrap(), 303 + ) 304 + .unwrap(); 305 + 306 + assert_eq!(result, ()); 307 + mock.assert(); 308 + } 309 + 310 + #[tokio::test] 311 + async fn remove_owner_success() { 312 + let key = "my-api-key-here"; 313 + let package = "gleam_experimental_stdlib"; 314 + let owner = "lpil"; 315 + 316 + let mut server = mockito::Server::new_async().await; 317 + let mock = server 318 + .mock( 319 + "DELETE", 320 + format!("/packages/{}/owners/{}", package, owner).as_str(), 321 + ) 322 + .expect(1) 323 + .match_header("authorization", key) 324 + .match_header("accept", "application/json") 325 + .with_status(204) 326 + .create_async() 327 + .await; 328 + 329 + let mut config = Config::new(); 330 + config.api_base = http::Uri::try_from(server.url()).unwrap(); 331 + 332 + let result = crate::api_remove_owner_response( 333 + http_send(crate::api_remove_owner_request( 334 + package, owner, key, &config, 335 + )) 336 + .await 337 + .unwrap(), 338 + ) 339 + .unwrap(); 340 + 341 + assert_eq!(result, ()); 342 + mock.assert(); 343 + } 344 + 345 + #[tokio::test] 346 + async fn remove_key_success() { 347 + let name = "some-key-name"; 348 + let key = "my-api-key-here"; 349 + 350 + let mut server = mockito::Server::new_async().await; 351 + let mock = server 352 + .mock("DELETE", format!("/keys/{}", name).as_str()) 353 + .expect(1) 354 + .match_header("authorization", key) 355 + .match_header("accept", "application/json") 356 + .with_status(204) 357 + .create_async() 358 + .await; 359 + 360 + let mut config = Config::new(); 361 + config.api_base = http::Uri::try_from(server.url()).unwrap(); 362 + 363 + let result = crate::api_remove_api_key_response( 364 + http_send(crate::api_remove_api_key_request(name, key, &config)) 365 + .await 366 + .unwrap(), 367 + ) 368 + .unwrap(); 369 + 370 + assert_eq!(result, ()); 371 + mock.assert(); 372 + } 373 + 374 + #[tokio::test] 375 + async fn remove_key_success_2() { 376 + let name = "some-key-name"; 377 + let key = "my-api-key-here"; 378 + 379 + let mut server = mockito::Server::new_async().await; 380 + let mock = server 381 + .mock("DELETE", format!("/keys/{}", name).as_str()) 382 + .expect(1) 383 + .match_header("authorization", key) 384 + .match_header("accept", "application/json") 385 + .with_status(200) 386 + .create_async() 387 + .await; 388 + 389 + let mut config = Config::new(); 390 + config.api_base = http::Uri::try_from(server.url()).unwrap(); 391 + 392 + let result = crate::api_remove_api_key_response( 393 + http_send(crate::api_remove_api_key_request(name, key, &config)) 394 + .await 395 + .unwrap(), 396 + ) 397 + .unwrap(); 398 + 399 + assert_eq!(result, ()); 400 + mock.assert(); 401 + } 402 + 403 + #[tokio::test] 404 + async fn remove_docs_unknown_package_version() { 405 + let key = "my-api-key-here"; 406 + let package = "gleam_experimental_stdlib_this_does_not_exist"; 407 + let version = "0.8.0"; 408 + 409 + let mut server = mockito::Server::new_async().await; 410 + let mock = server 411 + .mock( 412 + "DELETE", 413 + format!("/packages/{}/releases/{}/docs", package, version).as_str(), 414 + ) 415 + .expect(1) 416 + .match_header("authorization", key) 417 + .match_header("accept", "application/json") 418 + .with_status(404) 419 + .create_async() 420 + .await; 421 + 422 + let mut config = Config::new(); 423 + config.api_base = http::Uri::try_from(server.url()).unwrap(); 424 + 425 + let result = crate::api_remove_docs_response( 426 + http_send(crate::api_remove_docs_request(package, version, key, &config).unwrap()) 427 + .await 428 + .unwrap(), 429 + ) 430 + .unwrap_err(); 431 + 432 + match result { 433 + ApiError::NotFound => (), 434 + result => panic!("expected ApiError::NotFound got {:?}", result), 435 + } 436 + 437 + mock.assert(); 438 + } 439 + 440 + #[tokio::test] 441 + async fn remove_docs_rate_limted() { 442 + let key = "my-api-key-here"; 443 + let package = "gleam_experimental_stdlib"; 444 + let version = "0.8.0"; 445 + 446 + let mut server = mockito::Server::new_async().await; 447 + let mock = server 448 + .mock( 449 + "DELETE", 450 + format!("/packages/{}/releases/{}/docs", package, version).as_str(), 451 + ) 452 + .expect(1) 453 + .match_header("authorization", key) 454 + .match_header("accept", "application/json") 455 + .with_status(429) 456 + .create_async() 457 + .await; 458 + 459 + let mut config = Config::new(); 460 + config.api_base = http::Uri::try_from(server.url()).unwrap(); 461 + 462 + let result = crate::api_remove_docs_response( 463 + http_send(crate::api_remove_docs_request(package, version, key, &config).unwrap()) 464 + .await 465 + .unwrap(), 466 + ) 467 + .unwrap_err(); 468 + 469 + match result { 470 + ApiError::RateLimited => (), 471 + result => panic!("expected ApiError::RateLimited got {:?}", result), 472 + } 473 + 474 + mock.assert(); 475 + } 476 + 477 + #[tokio::test] 478 + async fn remove_docs_invalid_key() { 479 + let key = "my-api-key-here"; 480 + let package = "gleam_experimental_stdlib"; 481 + let version = "0.8.0"; 482 + 483 + let mut server = mockito::Server::new_async().await; 484 + let mock = server 485 + .mock( 486 + "DELETE", 487 + format!("/packages/{}/releases/{}/docs", package, version).as_str(), 488 + ) 489 + .expect(1) 490 + .match_header("authorization", key) 491 + .match_header("accept", "application/json") 492 + .with_status(401) 493 + .with_body( 494 + json!({ 495 + "message": "invalid API key", 496 + "status": 401, 497 + }) 498 + .to_string(), 499 + ) 500 + .create_async() 501 + .await; 502 + 503 + let mut config = Config::new(); 504 + config.api_base = http::Uri::try_from(server.url()).unwrap(); 505 + 506 + let result = crate::api_remove_docs_response( 507 + http_send(crate::api_remove_docs_request(package, version, key, &config).unwrap()) 508 + .await 509 + .unwrap(), 510 + ) 511 + .unwrap_err(); 512 + 513 + match result { 514 + ApiError::InvalidApiKey => (), 515 + result => panic!("expected ApiError::InvalidApiKey got {:?}", result), 516 + } 517 + 518 + mock.assert(); 519 + } 520 + 521 + #[tokio::test] 522 + async fn remove_docs_forbidden() { 523 + let key = "my-api-key-here"; 524 + let package = "jason"; 525 + let version = "1.2.0"; 526 + 527 + let mut server = mockito::Server::new_async().await; 528 + let mock = server 529 + .mock( 530 + "DELETE", 531 + format!("/packages/{}/releases/{}/docs", package, version).as_str(), 532 + ) 533 + .expect(1) 534 + .match_header("authorization", key) 535 + .match_header("accept", "application/json") 536 + .with_status(403) 537 + .with_body( 538 + json!({ 539 + "message": "account is not authorized for this action", 540 + "status": 403, 541 + }) 542 + .to_string(), 543 + ) 544 + .create_async() 545 + .await; 546 + 547 + let mut config = Config::new(); 548 + config.api_base = http::Uri::try_from(server.url()).unwrap(); 549 + 550 + let result = crate::api_remove_docs_response( 551 + http_send(crate::api_remove_docs_request(package, version, key, &config).unwrap()) 552 + .await 553 + .unwrap(), 554 + ) 555 + .unwrap_err(); 556 + 557 + match result { 558 + ApiError::Forbidden => (), 559 + result => panic!("expected ApiError::Forbidden got {:?}", result), 560 + } 561 + 562 + mock.assert(); 563 + } 564 + 565 + #[tokio::test] 566 + async fn remove_docs_bad_package_name() { 567 + let key = "my-api-key-here"; 568 + let package = "not valid"; 569 + let version = "1.2.0"; 570 + 571 + let config = Config::new(); 572 + 573 + match crate::api_remove_docs_request(package, version, key, &config).unwrap_err() { 574 + ApiError::InvalidPackageNameFormat(p) if p == package => (), 575 + result => panic!("expected Err(ApiError::BadPackage), got {:?}", result), 576 + } 577 + } 578 + 579 + #[tokio::test] 580 + async fn publish_docs_success() { 581 + let key = "my-api-key-here"; 582 + let package = "gleam_experimental_stdlib_123"; 583 + let version = "0.8.0"; 584 + let tarball = std::include_bytes!("../test/example.tar.gz").to_vec(); 585 + 586 + let mut server = mockito::Server::new_async().await; 587 + let mock = server 588 + .mock( 589 + "POST", 590 + format!("/packages/{}/releases/{}/docs", package, version).as_str(), 591 + ) 592 + .expect(1) 593 + .match_header("authorization", key) 594 + .match_header("accept", "application/json") 595 + .with_status(201) 596 + .create_async() 597 + .await; 598 + 599 + let mut config = Config::new(); 600 + config.api_base = http::Uri::try_from(server.url()).unwrap(); 601 + 602 + let result = crate::api_publish_docs_response( 603 + http_send( 604 + crate::api_publish_docs_request(package, version, tarball, key, &config).unwrap(), 605 + ) 606 + .await 607 + .unwrap(), 608 + ); 609 + 610 + match result { 611 + Ok(()) => (), 612 + result => panic!("expected Ok(()), got {:?}", result), 613 + } 614 + 615 + mock.assert() 616 + } 617 + 618 + #[tokio::test] 619 + async fn publish_docs_bad_package_name() { 620 + let key = "my-api-key-here"; 621 + let package = "not valid"; 622 + let version = "1.2.0"; 623 + let tarball = std::include_bytes!("../test/example.tar.gz").to_vec(); 624 + 625 + let config = Config::new(); 626 + 627 + match crate::api_publish_docs_request(package, version, tarball, key, &config).unwrap_err() { 628 + ApiError::InvalidPackageNameFormat(p) if p == package => (), 629 + result => panic!("expected Err(ApiError::BadPackage), got {:?}", result), 630 + } 631 + } 632 + 633 + #[tokio::test] 634 + async fn publish_docs_bad_package_version() { 635 + let key = "my-api-key-here"; 636 + let package = "name"; 637 + let version = "invalid version"; 638 + let tarball = std::include_bytes!("../test/example.tar.gz").to_vec(); 639 + 640 + let config = Config::new(); 641 + 642 + match crate::api_publish_docs_request(package, version, tarball, key, &config).unwrap_err() { 643 + ApiError::InvalidVersionFormat(v) if v == version => (), 644 + result => panic!("expected ApiError::BadPackage, got {:?}", result), 645 + } 646 + } 647 + 648 + #[tokio::test] 649 + async fn publish_docs_not_found() { 650 + let key = "my-api-key-here"; 651 + let package = "name"; 652 + let version = "1.1.0"; 653 + let tarball = std::include_bytes!("../test/example.tar.gz").to_vec(); 654 + 655 + let mut server = mockito::Server::new_async().await; 656 + let mock = server 657 + .mock( 658 + "POST", 659 + format!("/packages/{}/releases/{}/docs", package, version).as_str(), 660 + ) 661 + .expect(1) 662 + .match_header("authorization", key) 663 + .match_header("accept", "application/json") 664 + .with_status(404) 665 + .create_async() 666 + .await; 667 + 668 + let mut config = Config::new(); 669 + config.api_base = http::Uri::try_from(server.url()).unwrap(); 670 + 671 + let result = crate::api_publish_docs_response( 672 + http_send( 673 + crate::api_publish_docs_request(package, version, tarball, key, &config).unwrap(), 674 + ) 675 + .await 676 + .unwrap(), 677 + ); 678 + 679 + match result { 680 + Err(ApiError::NotFound) => (), 681 + result => panic!("expected ApiError::NotFound, got {:?}", result), 682 + } 683 + 684 + mock.assert() 685 + } 686 + 687 + #[tokio::test] 688 + async fn publish_docs_rate_limit() { 689 + let key = "my-api-key-here"; 690 + let package = "name"; 691 + let version = "1.1.0"; 692 + let tarball = std::include_bytes!("../test/example.tar.gz").to_vec(); 693 + 694 + let mut server = mockito::Server::new_async().await; 695 + let mock = server 696 + .mock( 697 + "POST", 698 + format!("/packages/{}/releases/{}/docs", package, version).as_str(), 699 + ) 700 + .expect(1) 701 + .match_header("authorization", key) 702 + .match_header("accept", "application/json") 703 + .with_status(429) 704 + .create_async() 705 + .await; 706 + 707 + let mut config = Config::new(); 708 + config.api_base = http::Uri::try_from(server.url()).unwrap(); 709 + 710 + let result = crate::api_publish_docs_response( 711 + http_send( 712 + crate::api_publish_docs_request(package, version, tarball, key, &config).unwrap(), 713 + ) 714 + .await 715 + .unwrap(), 716 + ); 717 + 718 + match result { 719 + Err(ApiError::RateLimited) => (), 720 + result => panic!("expected ApiError::RateLimited, got {:?}", result), 721 + } 722 + 723 + mock.assert() 724 + } 725 + 726 + #[tokio::test] 727 + async fn publish_docs_invalid_api_key() { 728 + let key = "my-api-key-here"; 729 + let package = "gleam_experimental_stdlib"; 730 + let version = "0.8.0"; 731 + let tarball = std::include_bytes!("../test/example.tar.gz").to_vec(); 732 + 733 + let mut server = mockito::Server::new_async().await; 734 + let mock = server 735 + .mock( 736 + "POST", 737 + format!("/packages/{}/releases/{}/docs", package, version).as_str(), 738 + ) 739 + .expect(1) 740 + .match_header("authorization", key) 741 + .match_header("accept", "application/json") 742 + .with_status(401) 743 + .with_body( 744 + json!({ 745 + "message": "invalid API key", 746 + "status": 401, 747 + }) 748 + .to_string(), 749 + ) 750 + .create_async() 751 + .await; 752 + 753 + let mut config = Config::new(); 754 + config.api_base = http::Uri::try_from(server.url()).unwrap(); 755 + 756 + let result = crate::api_publish_docs_response( 757 + http_send( 758 + crate::api_publish_docs_request(package, version, tarball, key, &config).unwrap(), 759 + ) 760 + .await 761 + .unwrap(), 762 + ); 763 + 764 + match result { 765 + Err(ApiError::InvalidApiKey) => (), 766 + result => panic!("expected Err(ApiError::InvalidApiKey), got {:?}", result), 767 + } 768 + 769 + mock.assert(); 770 + } 771 + 772 + #[tokio::test] 773 + async fn publish_docs_forbidden() { 774 + let key = "my-api-key-here"; 775 + let package = "gleam_experimental_stdlib"; 776 + let version = "0.8.0"; 777 + let tarball = std::include_bytes!("../test/example.tar.gz").to_vec(); 778 + 779 + let mut server = mockito::Server::new_async().await; 780 + let mock = server 781 + .mock( 782 + "POST", 783 + format!("/packages/{}/releases/{}/docs", package, version).as_str(), 784 + ) 785 + .expect(1) 786 + .match_header("authorization", key) 787 + .match_header("accept", "application/json") 788 + .with_status(403) 789 + .with_body( 790 + json!({ 791 + "message": "account is not authorized for this action", 792 + "status": 403, 793 + }) 794 + .to_string(), 795 + ) 796 + .create_async() 797 + .await; 798 + 799 + let mut config = Config::new(); 800 + config.api_base = http::Uri::try_from(server.url()).unwrap(); 801 + 802 + let result = crate::api_publish_docs_response( 803 + http_send( 804 + crate::api_publish_docs_request(package, version, tarball, key, &config).unwrap(), 805 + ) 806 + .await 807 + .unwrap(), 808 + ); 809 + 810 + match result { 811 + Err(ApiError::Forbidden) => (), 812 + result => panic!("expected Err(ApiError::Forbidden), got {:?}", result), 813 + } 814 + 815 + mock.assert(); 816 + } 817 + 818 + fn expected_package_exfmt() -> Package { 819 + Package { 820 + name: "exfmt".to_string(), 821 + repository: "hexpm".to_string(), 822 + releases: vec![ 823 + Release { 824 + version: Version::try_from("0.0.0").unwrap(), 825 + requirements: [].into(), 826 + retirement_status: None, 827 + outer_checksum: vec![ 828 + 82, 48, 191, 145, 92, 172, 0, 108, 238, 71, 57, 23, 101, 177, 161, 83, 91, 182, 829 + 18, 232, 249, 225, 29, 12, 246, 5, 215, 165, 32, 57, 179, 110, 830 + ], 831 + meta: (), 832 + }, 833 + Release { 834 + version: Version::try_from("0.1.0").unwrap(), 835 + requirements: [].into(), 836 + retirement_status: None, 837 + outer_checksum: vec![ 838 + 111, 246, 240, 176, 118, 229, 12, 15, 164, 61, 186, 3, 89, 106, 153, 225, 247, 839 + 52, 245, 8, 216, 139, 21, 232, 200, 16, 214, 59, 241, 188, 9, 6, 840 + ], 841 + meta: (), 842 + }, 843 + Release { 844 + version: Version::try_from("0.2.0").unwrap(), 845 + requirements: [].into(), 846 + retirement_status: None, 847 + outer_checksum: vec![ 848 + 149, 9, 192, 229, 84, 162, 110, 207, 161, 43, 31, 0, 126, 168, 14, 243, 31, 43, 849 + 195, 238, 100, 91, 78, 100, 213, 181, 101, 154, 106, 168, 170, 107, 850 + ], 851 + meta: (), 852 + }, 853 + Release { 854 + version: Version::try_from("0.2.1").unwrap(), 855 + requirements: [].into(), 856 + retirement_status: None, 857 + outer_checksum: vec![ 858 + 157, 229, 28, 212, 92, 249, 14, 240, 235, 104, 31, 12, 160, 199, 83, 195, 154, 859 + 105, 222, 37, 221, 80, 181, 183, 113, 240, 234, 107, 144, 85, 255, 65, 860 + ], 861 + meta: (), 862 + }, 863 + Release { 864 + version: Version::try_from("0.2.2").unwrap(), 865 + requirements: [].into(), 866 + retirement_status: None, 867 + outer_checksum: vec![ 868 + 112, 250, 133, 189, 183, 192, 54, 218, 115, 55, 216, 97, 204, 201, 191, 168, 869 + 250, 133, 138, 252, 202, 240, 74, 197, 228, 235, 81, 18, 241, 7, 155, 38, 870 + ], 871 + meta: (), 872 + }, 873 + Release { 874 + version: Version::try_from("0.2.3").unwrap(), 875 + requirements: [].into(), 876 + retirement_status: None, 877 + outer_checksum: vec![ 878 + 131, 20, 29, 160, 171, 124, 7, 125, 210, 88, 17, 189, 199, 49, 191, 190, 14, 879 + 162, 38, 247, 52, 176, 189, 17, 7, 188, 151, 152, 24, 64, 170, 29, 880 + ], 881 + meta: (), 882 + }, 883 + Release { 884 + version: Version::try_from("0.2.4").unwrap(), 885 + requirements: [].into(), 886 + retirement_status: None, 887 + outer_checksum: vec![ 888 + 109, 162, 185, 169, 26, 4, 62, 60, 167, 54, 182, 161, 140, 197, 75, 113, 183, 889 + 117, 247, 201, 218, 228, 14, 160, 115, 157, 196, 51, 108, 16, 96, 217, 890 + ], 891 + meta: (), 892 + }, 893 + Release { 894 + version: Version::try_from("0.3.0").unwrap(), 895 + requirements: [].into(), 896 + retirement_status: None, 897 + outer_checksum: vec![ 898 + 97, 50, 95, 212, 242, 59, 245, 177, 140, 78, 79, 180, 108, 174, 119, 176, 24, 899 + 80, 218, 152, 178, 227, 152, 242, 32, 126, 72, 67, 222, 0, 173, 170, 900 + ], 901 + meta: (), 902 + }, 903 + Release { 904 + version: Version::try_from("0.4.0").unwrap(), 905 + requirements: [].into(), 906 + retirement_status: None, 907 + outer_checksum: vec![ 908 + 246, 178, 237, 214, 217, 158, 143, 52, 130, 186, 64, 50, 94, 175, 161, 81, 68, 909 + 186, 4, 73, 53, 226, 235, 144, 209, 84, 231, 136, 165, 119, 122, 126, 910 + ], 911 + meta: (), 912 + }, 913 + Release { 914 + version: Version::try_from("0.5.0").unwrap(), 915 + requirements: [].into(), 916 + retirement_status: None, 917 + outer_checksum: vec![ 918 + 151, 86, 157, 218, 218, 131, 240, 119, 198, 216, 202, 240, 65, 17, 57, 228, 84, 919 + 252, 59, 207, 246, 49, 22, 21, 52, 47, 51, 139, 190, 9, 95, 109, 920 + ], 921 + meta: (), 922 + }, 923 + ], 924 + } 925 + } 926 + 927 + #[tokio::test] 928 + async fn get_package_ok_test() { 929 + let response_body = std::include_bytes!("../test/package_exfmt"); 930 + 931 + // Set up test server 932 + let mut server = mockito::Server::new_async().await; 933 + let mock = server 934 + .mock("GET", "/packages/exfmt") 935 + .expect(1) 936 + .with_status(200) 937 + .with_body(&response_body[..]) 938 + .create_async() 939 + .await; 940 + 941 + // Test! 942 + let mut config = Config::new(); 943 + config.repository_base = http::Uri::try_from(server.url()).unwrap(); 944 + 945 + let package = crate::repository_v2_get_package_response( 946 + http_send(crate::repository_v2_get_package_request( 947 + "exfmt", None, &config, 948 + )) 949 + .await 950 + .unwrap(), 951 + std::include_bytes!("../test/public_key"), 952 + ) 953 + .unwrap(); 954 + 955 + assert_eq!(expected_package_exfmt(), package,); 956 + 957 + mock.assert(); 958 + } 959 + 960 + #[tokio::test] 961 + async fn get_package_not_found() { 962 + let config = Config::new(); 963 + let error = crate::repository_v2_get_package_response( 964 + http_send(crate::repository_v2_get_package_request( 965 + "louissaysthispackagedoesnotexist", 966 + None, 967 + &config, 968 + )) 969 + .await 970 + .unwrap(), 971 + std::include_bytes!("../test/public_key"), 972 + ) 973 + .unwrap_err(); 974 + 975 + assert!(error.is_not_found()); 976 + } 977 + 978 + #[tokio::test] 979 + async fn get_package_from_bytes_ok() { 980 + let response_body = std::include_bytes!("../test/package_exfmt"); 981 + let mut uncompressed = Vec::new(); 982 + let mut decoder = GzDecoder::new(Cursor::new(response_body)); 983 + let _ = decoder 984 + .read_to_end(&mut uncompressed) 985 + .expect("failed to decompress body"); 986 + 987 + let package = crate::repository_v2_package_parse_body( 988 + &uncompressed, 989 + std::include_bytes!("../test/public_key"), 990 + ) 991 + .expect("package failed to parse"); 992 + 993 + assert_eq!(expected_package_exfmt(), package,); 994 + } 995 + 996 + #[tokio::test] 997 + async fn get_package_from_bytes_malformed() { 998 + // public key should not be a valid protobuf and should therefore fail 999 + let bytes = std::include_bytes!("../test/public_key").to_vec(); 1000 + let package_error = crate::repository_v2_package_parse_body(&bytes, &bytes) 1001 + .expect_err("parsing failed to fail"); 1002 + 1003 + assert!(package_error.is_invalid_protobuf()); 1004 + } 1005 + 1006 + #[tokio::test] 1007 + async fn get_repository_versions_ok_test() { 1008 + let response_body = std::include_bytes!("../test/versions"); 1009 + 1010 + // Set up test server 1011 + let mut server = mockito::Server::new_async().await; 1012 + let mock = server 1013 + .mock("GET", "/versions") 1014 + .expect(1) 1015 + .with_status(200) 1016 + .with_body(&response_body[..]) 1017 + .create_async() 1018 + .await; 1019 + 1020 + // Test! 1021 + let mut config = Config::new(); 1022 + config.repository_base = http::Uri::try_from(server.url()).unwrap(); 1023 + 1024 + let versions = crate::repository_v2_get_versions_response( 1025 + http_send(crate::repository_v2_get_versions_request(None, &config)) 1026 + .await 1027 + .unwrap(), 1028 + std::include_bytes!("../test/public_key"), 1029 + ); 1030 + 1031 + assert_eq!( 1032 + &vec![ 1033 + Version::parse("0.0.0").unwrap(), 1034 + Version::parse("0.1.0").unwrap(), 1035 + Version::parse("0.2.0").unwrap(), 1036 + Version::parse("0.2.1").unwrap(), 1037 + Version::parse("0.2.2").unwrap(), 1038 + Version::parse("0.2.3").unwrap(), 1039 + Version::parse("0.2.4").unwrap(), 1040 + Version::parse("0.3.0").unwrap(), 1041 + Version::parse("0.4.0").unwrap(), 1042 + Version::parse("0.5.0").unwrap(), 1043 + ], 1044 + versions.unwrap().get("exfmt").unwrap(), 1045 + ); 1046 + 1047 + mock.assert(); 1048 + } 1049 + 1050 + #[tokio::test] 1051 + async fn get_repository_versions_from_bytes_ok() { 1052 + let response_body = std::include_bytes!("../test/versions"); 1053 + let mut uncompressed = Vec::new(); 1054 + let mut decoder = GzDecoder::new(Cursor::new(response_body)); 1055 + let _ = decoder 1056 + .read_to_end(&mut uncompressed) 1057 + .expect("failed to decompress body"); 1058 + 1059 + let versions = crate::repository_v2_get_versions_body( 1060 + &uncompressed, 1061 + std::include_bytes!("../test/public_key"), 1062 + ) 1063 + .expect("versions failed to parse"); 1064 + 1065 + assert_eq!( 1066 + &vec![ 1067 + Version::parse("0.0.0").unwrap(), 1068 + Version::parse("0.1.0").unwrap(), 1069 + Version::parse("0.2.0").unwrap(), 1070 + Version::parse("0.2.1").unwrap(), 1071 + Version::parse("0.2.2").unwrap(), 1072 + Version::parse("0.2.3").unwrap(), 1073 + Version::parse("0.2.4").unwrap(), 1074 + Version::parse("0.3.0").unwrap(), 1075 + Version::parse("0.4.0").unwrap(), 1076 + Version::parse("0.5.0").unwrap(), 1077 + ], 1078 + versions.get("exfmt").unwrap(), 1079 + ); 1080 + } 1081 + 1082 + #[tokio::test] 1083 + async fn get_repository_versions_from_bytes_malformed() { 1084 + // public key should not be a valid protobuf and should therefore fail 1085 + let bytes = std::include_bytes!("../test/public_key").to_vec(); 1086 + let versions_error = 1087 + crate::repository_v2_get_versions_body(&bytes, &bytes).expect_err("parsing failed to fail"); 1088 + 1089 + assert!(versions_error.is_invalid_protobuf()); 1090 + } 1091 + 1092 + #[tokio::test] 1093 + async fn get_repository_tarball_ok_test() { 1094 + let config = Config::new(); 1095 + let checksum = 1096 + base16::decode("9107f6a859cb96945ad9a099085db028ca2bebb3c8ea42eec227b51c614cc2e0").unwrap(); 1097 + 1098 + let downloaded = crate::repository_get_package_tarball_response( 1099 + http_send(crate::repository_get_package_tarball_request( 1100 + "gleam_stdlib", 1101 + "0.14.0", 1102 + None, 1103 + &config, 1104 + )) 1105 + .await 1106 + .unwrap(), 1107 + &checksum, 1108 + ) 1109 + .unwrap(); 1110 + 1111 + assert_eq!( 1112 + &downloaded, 1113 + std::include_bytes!("../test/gleam_stdlib-0.14.0.tar") 1114 + ); 1115 + } 1116 + 1117 + #[tokio::test] 1118 + async fn get_repository_tarball_bad_checksum_test() { 1119 + let config = Config::new(); 1120 + let checksum = vec![1, 2, 3, 4, 5]; 1121 + 1122 + let err = crate::repository_get_package_tarball_response( 1123 + http_send(crate::repository_get_package_tarball_request( 1124 + "gleam_stdlib", 1125 + "0.14.0", 1126 + None, 1127 + &config, 1128 + )) 1129 + .await 1130 + .unwrap(), 1131 + &checksum, 1132 + ) 1133 + .unwrap_err(); 1134 + 1135 + assert_eq!( 1136 + err.to_string(), 1137 + "the downloaded data did not have the expected checksum" 1138 + ); 1139 + } 1140 + 1141 + #[tokio::test] 1142 + async fn get_repository_tarball_not_found_test() { 1143 + let config = Config::new(); 1144 + let checksum = vec![1, 2, 3, 4, 5]; 1145 + 1146 + let err = crate::repository_get_package_tarball_response( 1147 + http_send(crate::repository_get_package_tarball_request( 1148 + "gleam_stdlib", 1149 + "unknown-version", 1150 + None, 1151 + &config, 1152 + )) 1153 + .await 1154 + .unwrap(), 1155 + &checksum, 1156 + ) 1157 + .unwrap_err(); 1158 + 1159 + assert_eq!(err.to_string(), "resource was not found"); 1160 + } 1161 + 1162 + #[tokio::test] 1163 + async fn publish_package_success() { 1164 + let key = "my-api-key-here"; 1165 + let tarball = std::include_bytes!("../test/example.tar.gz").to_vec(); 1166 + 1167 + let mut server = mockito::Server::new_async().await; 1168 + let mock = server 1169 + .mock("POST", "/publish?replace=false") 1170 + .expect(1) 1171 + .match_header("authorization", key) 1172 + .match_header("accept", "application/json") 1173 + .with_status(201) 1174 + .create_async() 1175 + .await; 1176 + 1177 + let mut config = Config::new(); 1178 + config.api_base = http::Uri::try_from(server.url()).unwrap(); 1179 + 1180 + let result = crate::api_publish_package_response( 1181 + http_send(crate::api_publish_package_request( 1182 + tarball, key, &config, false, 1183 + )) 1184 + .await 1185 + .unwrap(), 1186 + ); 1187 + 1188 + match result { 1189 + Ok(()) => (), 1190 + result => panic!("expected Ok(()), got {:?}", result), 1191 + } 1192 + 1193 + mock.assert() 1194 + } 1195 + 1196 + #[tokio::test] 1197 + async fn modify_package_late() { 1198 + let key = "my-api-key-here"; 1199 + let tarball = std::include_bytes!("../test/example.tar.gz").to_vec(); 1200 + 1201 + let mut server = mockito::Server::new_async().await; 1202 + let mock = server 1203 + .mock("POST", "/publish?replace=true") 1204 + .expect(1) 1205 + .match_header("authorization", key) 1206 + .match_header("accept", "application/json") 1207 + .with_status(422) 1208 + .with_body( 1209 + json!({ 1210 + "errors": {"inserted_at": "can only modify a release up to one hour after publication"}, 1211 + "message": "Validation error(s)", 1212 + "status": 422, 1213 + }) 1214 + .to_string(), 1215 + ) 1216 + .create_async().await; 1217 + 1218 + let mut config = Config::new(); 1219 + config.api_base = http::Uri::try_from(server.url()).unwrap(); 1220 + 1221 + let result = crate::api_publish_package_response( 1222 + http_send(crate::api_publish_package_request( 1223 + tarball, key, &config, true, 1224 + )) 1225 + .await 1226 + .unwrap(), 1227 + ); 1228 + 1229 + match result { 1230 + Err(ApiError::LateModification) => (), 1231 + result => panic!("expected Err(ApiError::LateModification), got {:?}", result), 1232 + } 1233 + 1234 + mock.assert() 1235 + } 1236 + 1237 + #[tokio::test] 1238 + async fn not_replacing() { 1239 + let key = "my-api-key-here"; 1240 + let tarball = std::include_bytes!("../test/example.tar.gz").to_vec(); 1241 + 1242 + let mut server = mockito::Server::new_async().await; 1243 + let mock = server 1244 + .mock("POST", "/publish?replace=false") 1245 + .expect(1) 1246 + .match_header("authorization", key) 1247 + .match_header("accept", "application/json") 1248 + .with_status(422) 1249 + .with_body( 1250 + json!({ 1251 + "errors": {"inserted_at": "must include the --replace flag to update an existing release"}, 1252 + "message": "Validation error(s)", 1253 + "status": 422, 1254 + }) 1255 + .to_string(), 1256 + ) 1257 + .create_async().await; 1258 + 1259 + let mut config = Config::new(); 1260 + config.api_base = http::Uri::try_from(server.url()).unwrap(); 1261 + 1262 + let result = crate::api_publish_package_response( 1263 + http_send(crate::api_publish_package_request( 1264 + tarball, key, &config, false, 1265 + )) 1266 + .await 1267 + .unwrap(), 1268 + ); 1269 + 1270 + match result { 1271 + Err(ApiError::NotReplacing) => (), 1272 + result => panic!("expected Err(ApiError::NotReplacing), got {:?}", result), 1273 + } 1274 + 1275 + mock.assert() 1276 + } 1277 + 1278 + #[tokio::test] 1279 + async fn get_package_release_not_found() { 1280 + let config = Config::new(); 1281 + let error = crate::api_get_package_release_response( 1282 + http_send(crate::api_get_package_release_request( 1283 + "louissaysthispackagedoesnotexist", 1284 + "1.0.1", 1285 + None, 1286 + &config, 1287 + )) 1288 + .await 1289 + .unwrap(), 1290 + ) 1291 + .unwrap_err(); 1292 + 1293 + assert!(error.is_not_found()); 1294 + } 1295 + 1296 + #[tokio::test] 1297 + async fn get_package_release_ok() { 1298 + let config = Config::new(); 1299 + let resp = crate::api_get_package_release_response( 1300 + http_send(crate::api_get_package_release_request( 1301 + "clint", "0.0.1", None, &config, 1302 + )) 1303 + .await 1304 + .unwrap(), 1305 + ) 1306 + .unwrap(); 1307 + 1308 + assert_eq!( 1309 + resp, 1310 + Release { 1311 + version: Version::new(0, 0, 1), 1312 + requirements: [ 1313 + ( 1314 + "plug".into(), 1315 + Dependency { 1316 + requirement: Range::new("~>0.11.0".into()).unwrap(), 1317 + optional: false, 1318 + app: Some("plug".into()), 1319 + repository: None 1320 + } 1321 + ), 1322 + ( 1323 + "cowboy".into(), 1324 + Dependency { 1325 + requirement: Range::new("~>1.0.0".into()).unwrap(), 1326 + optional: false, 1327 + app: Some("cowboy".into()), 1328 + repository: None 1329 + } 1330 + ) 1331 + ] 1332 + .into(), 1333 + retirement_status: None, 1334 + outer_checksum: vec![ 1335 + 65, 198, 120, 27, 95, 75, 152, 107, 206, 20, 195, 87, 141, 57, 196, 151, 188, 184, 1336 + 66, 127, 29, 54, 216, 205, 229, 252, 170, 110, 3, 202, 226, 177 1337 + ], 1338 + meta: ReleaseMeta { 1339 + app: "clint".into(), 1340 + build_tools: vec!["mix".into()] 1341 + } 1342 + } 1343 + ) 1344 + } 1345 + 1346 + #[test] 1347 + fn make_request_base_trailing_slash_is_optional() { 1348 + let slash = http::Uri::from_static("http://host/path/"); 1349 + let no_slash = http::Uri::from_static("http://host/path"); 1350 + let suffix = "suffix"; 1351 + let expect = "/path/suffix"; 1352 + 1353 + let slash = make_request(slash, http::Method::GET, suffix, None); 1354 + assert_eq!(slash.uri_ref().unwrap().path(), expect); 1355 + 1356 + let no_slash = make_request(no_slash, http::Method::GET, suffix, None); 1357 + assert_eq!(no_slash.uri_ref().unwrap().path(), expect); 1358 + }
+355
hexpm/src/version.rs
··· 1 + //! Functions for parsing and matching versions against requirements, based off 2 + //! and compatible with the Elixir Version module, which is used by Hex 3 + //! internally as well as be the Elixir build tool Hex client. 4 + 5 + use std::{cmp::Ordering, convert::TryFrom, fmt}; 6 + 7 + use self::parser::Parser; 8 + use serde::{ 9 + Deserialize, Serialize, 10 + de::{self, Deserializer, Visitor}, 11 + }; 12 + 13 + mod lexer; 14 + mod parser; 15 + #[cfg(test)] 16 + mod tests; 17 + 18 + /// In a nutshell, a version is represented by three numbers: 19 + /// 20 + /// MAJOR.MINOR.PATCH 21 + /// 22 + /// Pre-releases are supported by optionally appending a hyphen and a series of 23 + /// period-separated identifiers immediately following the patch version. 24 + /// Identifiers consist of only ASCII alphanumeric characters and hyphens (`[0-9A-Za-z-]`): 25 + /// 26 + /// "1.0.0-alpha.3" 27 + /// 28 + /// Build information can be added by appending a plus sign and a series of 29 + /// dot-separated identifiers immediately following the patch or pre-release version. 30 + /// Identifiers consist of only ASCII alphanumeric characters and hyphens (`[0-9A-Za-z-]`): 31 + /// 32 + /// "1.0.0-alpha.3+20130417140000.amd64" 33 + /// 34 + #[derive(Clone, Debug, PartialEq, Eq, Hash)] 35 + pub struct Version { 36 + pub major: u32, 37 + pub minor: u32, 38 + pub patch: u32, 39 + pub pre: Vec<Identifier>, 40 + pub build: Option<String>, 41 + } 42 + 43 + impl Version { 44 + pub fn new(major: u32, minor: u32, patch: u32) -> Self { 45 + Self { 46 + major, 47 + minor, 48 + patch, 49 + pre: vec![], 50 + build: None, 51 + } 52 + } 53 + 54 + fn bump_major(&self) -> Self { 55 + Self { 56 + major: self.major + 1, 57 + minor: 0, 58 + patch: 0, 59 + pre: vec![], 60 + build: None, 61 + } 62 + } 63 + 64 + fn bump_minor(&self) -> Self { 65 + Self { 66 + major: self.major, 67 + minor: self.minor + 1, 68 + patch: 0, 69 + pre: vec![], 70 + build: None, 71 + } 72 + } 73 + 74 + fn bump_patch(&self) -> Self { 75 + Self { 76 + major: self.major, 77 + minor: self.minor, 78 + patch: self.patch + 1, 79 + pre: vec![], 80 + build: None, 81 + } 82 + } 83 + 84 + /// Parse a version. 85 + pub fn parse(input: &str) -> Result<Self, parser::Error> { 86 + let mut parser = Parser::new(input)?; 87 + let version = parser.version()?; 88 + if !parser.is_eof() { 89 + return Err(parser::Error::MoreInput( 90 + parser 91 + .tail()? 92 + .into_iter() 93 + .map(|t| t.to_string()) 94 + .collect::<Vec<_>>() 95 + .join(""), 96 + )); 97 + } 98 + Ok(version) 99 + } 100 + 101 + /// Parse a Hex compatible version range. i.e. `> 1 and < 2 or == 4.5.2`. 102 + fn parse_range(input: &str) -> Result<pubgrub::Range<Version>, parser::Error> { 103 + let mut parser = Parser::new(input)?; 104 + let version = parser.range()?; 105 + if !parser.is_eof() { 106 + return Err(parser::Error::MoreInput( 107 + parser 108 + .tail()? 109 + .into_iter() 110 + .map(|t| t.to_string()) 111 + .collect::<Vec<_>>() 112 + .join(""), 113 + )); 114 + } 115 + Ok(version) 116 + } 117 + 118 + pub fn lowest() -> Self { 119 + Self::new(0, 0, 0) 120 + } 121 + 122 + fn tuple(&self) -> (u32, u32, u32, PreOrder<'_>) { 123 + ( 124 + self.major, 125 + self.minor, 126 + self.patch, 127 + PreOrder(self.pre.as_slice()), 128 + ) 129 + } 130 + 131 + pub fn is_pre(&self) -> bool { 132 + !self.pre.is_empty() 133 + } 134 + } 135 + 136 + pub trait LowestVersion { 137 + fn lowest_version(&self) -> Option<Version>; 138 + } 139 + impl LowestVersion for pubgrub::Range<Version> { 140 + fn lowest_version(&self) -> Option<Version> { 141 + self.iter() 142 + .flat_map(|(lower, _higher)| match lower { 143 + std::ops::Bound::Included(v) => Some(v.clone()), 144 + std::ops::Bound::Excluded(_) => None, 145 + std::ops::Bound::Unbounded => Some(Version::lowest()), 146 + }) 147 + .min() 148 + } 149 + } 150 + 151 + impl<'de> Deserialize<'de> for Version { 152 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 153 + where 154 + D: Deserializer<'de>, 155 + { 156 + deserializer.deserialize_str(VersionVisitor) 157 + } 158 + } 159 + 160 + struct VersionVisitor; 161 + 162 + impl<'de> Visitor<'de> for VersionVisitor { 163 + type Value = Version; 164 + 165 + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 166 + formatter.write_str("a Hex version string") 167 + } 168 + 169 + fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> 170 + where 171 + E: de::Error, 172 + { 173 + Version::try_from(value).map_err(de::Error::custom) 174 + } 175 + } 176 + 177 + impl Serialize for Version { 178 + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 179 + where 180 + S: serde::Serializer, 181 + { 182 + serializer.serialize_str(&self.to_string()) 183 + } 184 + } 185 + 186 + impl std::cmp::PartialOrd for Version { 187 + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { 188 + Some(self.cmp(other)) 189 + } 190 + } 191 + 192 + impl std::cmp::Ord for Version { 193 + fn cmp(&self, other: &Self) -> Ordering { 194 + self.tuple().cmp(&other.tuple()) 195 + } 196 + } 197 + 198 + impl<'a> TryFrom<&'a str> for Version { 199 + type Error = parser::Error; 200 + 201 + fn try_from(value: &'a str) -> Result<Self, Self::Error> { 202 + Self::parse(value) 203 + } 204 + } 205 + 206 + impl fmt::Display for Version { 207 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 208 + write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?; 209 + if !self.pre.is_empty() { 210 + write!(f, "-")?; 211 + for (i, identifier) in self.pre.iter().enumerate() { 212 + if i != 0 { 213 + write!(f, ".")?; 214 + } 215 + identifier.fmt(f)?; 216 + } 217 + } 218 + if let Some(build) = self.build.as_ref() { 219 + write!(f, "+{}", build)?; 220 + } 221 + Ok(()) 222 + } 223 + } 224 + 225 + #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] 226 + pub enum Identifier { 227 + Numeric(u32), 228 + AlphaNumeric(String), 229 + } 230 + 231 + impl fmt::Display for Identifier { 232 + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 233 + match *self { 234 + Identifier::Numeric(ref id) => id.fmt(f), 235 + Identifier::AlphaNumeric(ref id) => id.fmt(f), 236 + } 237 + } 238 + } 239 + 240 + impl Identifier { 241 + pub fn concat(self, add_str: &str) -> Identifier { 242 + match self { 243 + Identifier::Numeric(n) => Identifier::AlphaNumeric(format!("{}{}", n, add_str)), 244 + Identifier::AlphaNumeric(mut s) => { 245 + s.push_str(add_str); 246 + Identifier::AlphaNumeric(s) 247 + } 248 + } 249 + } 250 + } 251 + 252 + #[derive(Clone, PartialEq, Eq)] 253 + pub struct Range { 254 + spec: String, 255 + range: pubgrub::Range<Version>, 256 + } 257 + 258 + impl Range { 259 + pub fn new(spec: String) -> Result<Self, parser::Error> { 260 + let range = Version::parse_range(&spec)?; 261 + Ok(Self { spec, range }) 262 + } 263 + } 264 + 265 + impl Range { 266 + pub fn to_pubgrub(&self) -> &pubgrub::Range<Version> { 267 + &self.range 268 + } 269 + 270 + pub fn as_str(&self) -> &str { 271 + &self.spec 272 + } 273 + } 274 + 275 + impl From<pubgrub::Range<Version>> for Range { 276 + fn from(range: pubgrub::Range<Version>) -> Self { 277 + let spec = range.to_string(); 278 + Self { spec, range } 279 + } 280 + } 281 + 282 + impl From<Version> for Range { 283 + fn from(version: Version) -> Self { 284 + pubgrub::Range::singleton(version).into() 285 + } 286 + } 287 + 288 + impl From<Range> for pubgrub::Range<Version> { 289 + fn from(range: Range) -> Self { 290 + range.range 291 + } 292 + } 293 + 294 + impl fmt::Debug for Range { 295 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 296 + f.debug_tuple("Range").field(&self.spec).finish() 297 + } 298 + } 299 + 300 + impl<'de> Deserialize<'de> for Range { 301 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 302 + where 303 + D: Deserializer<'de>, 304 + { 305 + let s: &str = Deserialize::deserialize(deserializer)?; 306 + Range::new(s.to_string()).map_err(serde::de::Error::custom) 307 + } 308 + } 309 + 310 + impl Serialize for Range { 311 + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 312 + where 313 + S: serde::Serializer, 314 + { 315 + serializer.serialize_str(&self.to_string()) 316 + } 317 + } 318 + 319 + impl fmt::Display for Range { 320 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 321 + f.write_str(&self.spec) 322 + } 323 + } 324 + 325 + // A wrapper around Vec where an empty vector is greater than a non-empty one. 326 + // This is desires as if there is a pre-segment in a version (1.0.0-rc1) it is 327 + // lower than the same version with no pre-segments (1.0.0). 328 + #[derive(PartialEq, Eq)] 329 + pub struct PreOrder<'a>(&'a [Identifier]); 330 + 331 + impl PreOrder<'_> { 332 + fn is_empty(&self) -> bool { 333 + self.0.is_empty() 334 + } 335 + } 336 + 337 + impl std::cmp::PartialOrd for PreOrder<'_> { 338 + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { 339 + Some(self.cmp(other)) 340 + } 341 + } 342 + 343 + impl std::cmp::Ord for PreOrder<'_> { 344 + fn cmp(&self, other: &Self) -> Ordering { 345 + if self.is_empty() && other.is_empty() { 346 + Ordering::Equal 347 + } else if self.is_empty() { 348 + Ordering::Greater 349 + } else if other.is_empty() { 350 + Ordering::Less 351 + } else { 352 + self.0.cmp(other.0) 353 + } 354 + } 355 + }
+332
hexpm/src/version/lexer.rs
··· 1 + //! Lexer for semver ranges. 2 + //! 3 + //! Breaks a string of input into an iterator of tokens that can be used with a parser. 4 + //! 5 + //! Based off https://github.com/steveklabnik/semver-parser/blob/bee9de80aaa9653c5eb46a83658606cb21151e65/src/lexer.rs 6 + //! 7 + use self::Error::*; 8 + use self::Token::*; 9 + use std::str; 10 + 11 + macro_rules! scan_while { 12 + ($slf:expr, $start:expr, $first:pat_param $(| $rest:pat)*) => {{ 13 + let mut __end = $start; 14 + 15 + loop { 16 + if let Some((idx, c)) = $slf.one() { 17 + __end = idx; 18 + 19 + match c { 20 + $first $(| $rest)* => $slf.step(), 21 + _ => break, 22 + } 23 + 24 + continue; 25 + } else { 26 + __end = $slf.input.len(); 27 + } 28 + 29 + break; 30 + } 31 + 32 + __end 33 + }} 34 + } 35 + 36 + /// Semver tokens. 37 + #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] 38 + pub enum Token<'input> { 39 + /// `==` 40 + Eq, 41 + /// `!=` 42 + NotEq, 43 + /// `>` 44 + Gt, 45 + /// `<` 46 + Lt, 47 + /// `<=` 48 + LtEq, 49 + /// `>=` 50 + GtEq, 51 + /// '~>` 52 + Pessimistic, 53 + /// `.` 54 + Dot, 55 + /// `-` 56 + Hyphen, 57 + /// `+` 58 + Plus, 59 + /// 'or' 60 + Or, 61 + /// 'and' 62 + And, 63 + /// any number of whitespace (`\t\r\n `) and its span. 64 + Whitespace(usize, usize), 65 + /// Numeric component, like `0` or `42`. 66 + Numeric(u32), 67 + /// Alphanumeric component, like `alpha1` or `79deadbe`. 68 + AlphaNumeric(&'input str), 69 + /// An alphanumeric component with a leading zero, like `0alpha1` or `079deadbe`. 70 + LeadingZero(&'input str), 71 + } 72 + 73 + #[cfg(test)] 74 + impl<'input> Token<'input> { 75 + /// Check if the current token is a whitespace token. 76 + pub fn is_whitespace(&self) -> bool { 77 + match *self { 78 + Whitespace(..) => true, 79 + _ => false, 80 + } 81 + } 82 + } 83 + 84 + impl std::fmt::Display for Token<'_> { 85 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 86 + match self { 87 + Eq => write!(f, "=="), 88 + NotEq => write!(f, "!="), 89 + Gt => write!(f, ">"), 90 + Lt => write!(f, "<"), 91 + LtEq => write!(f, "<="), 92 + GtEq => write!(f, "<="), 93 + Pessimistic => write!(f, "~>"), 94 + Dot => write!(f, "."), 95 + Hyphen => write!(f, "-"), 96 + Plus => write!(f, "+"), 97 + Or => write!(f, "or"), 98 + And => write!(f, "and"), 99 + Whitespace(_, _) => write!(f, " "), 100 + Numeric(i) => write!(f, "{}", i), 101 + AlphaNumeric(a) => write!(f, "{}", a), 102 + LeadingZero(z) => write!(f, "{}", z), 103 + } 104 + } 105 + } 106 + 107 + #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, thiserror::Error)] 108 + pub enum Error { 109 + #[error("Unexpected character {0}")] 110 + UnexpectedChar(char), 111 + } 112 + 113 + /// Lexer for semver tokens belonging to a range. 114 + #[derive(Debug)] 115 + pub struct Lexer<'input> { 116 + input: &'input str, 117 + chars: str::CharIndices<'input>, 118 + // lookahead 119 + c1: Option<(usize, char)>, 120 + c2: Option<(usize, char)>, 121 + } 122 + 123 + impl<'input> Lexer<'input> { 124 + /// Construct a new lexer for the given input. 125 + pub fn new(input: &str) -> Lexer<'_> { 126 + let mut chars = input.char_indices(); 127 + let c1 = chars.next(); 128 + let c2 = chars.next(); 129 + 130 + Lexer { 131 + input, 132 + chars, 133 + c1, 134 + c2, 135 + } 136 + } 137 + 138 + /// Shift all lookahead storage by one. 139 + fn step(&mut self) { 140 + self.c1 = self.c2; 141 + self.c2 = self.chars.next(); 142 + } 143 + 144 + fn step_n(&mut self, n: usize) { 145 + for _ in 0..n { 146 + self.step(); 147 + } 148 + } 149 + 150 + /// Access the one character, or set it if it is not set. 151 + fn one(&mut self) -> Option<(usize, char)> { 152 + self.c1 153 + } 154 + 155 + /// Access two characters. 156 + fn two(&mut self) -> Option<(usize, char, char)> { 157 + self.c1 158 + .and_then(|(start, c1)| self.c2.map(|(_, c2)| (start, c1, c2))) 159 + } 160 + 161 + /// Consume a component. 162 + /// 163 + /// A component can either be an alphanumeric or numeric. 164 + /// Does not permit leading zeroes. 165 + fn component(&mut self, start: usize) -> Result<Token<'input>, Error> { 166 + let end = scan_while!(self, start, '0'..='9' | 'A'..='Z' | 'a'..='z'); 167 + let input = &self.input[start..end]; 168 + 169 + let mut it = input.chars(); 170 + let (a, b) = (it.next(), it.next()); 171 + 172 + // exactly zero 173 + if a == Some('0') && b.is_none() { 174 + return Ok(Numeric(0)); 175 + } 176 + 177 + if let Ok(numeric) = input.parse::<u32>() { 178 + // Only parse as a number if there is no leading zero 179 + if a != Some('0') { 180 + return Ok(Numeric(numeric)); 181 + } else { 182 + return Ok(LeadingZero(input)); 183 + } 184 + } 185 + 186 + Ok(AlphaNumeric(input)) 187 + } 188 + 189 + fn and(&mut self, start: usize) -> Result<Token<'input>, Error> { 190 + match self.one() { 191 + Some((_, 'd')) => { 192 + self.step(); 193 + Ok(And) 194 + } 195 + _ => self.component(start), 196 + } 197 + } 198 + 199 + /// Consume whitespace. 200 + fn whitespace(&mut self, start: usize) -> Result<Token<'input>, Error> { 201 + let end = scan_while!(self, start, ' ' | '\t' | '\n' | '\r'); 202 + Ok(Whitespace(start, end)) 203 + } 204 + } 205 + 206 + impl<'input> Iterator for Lexer<'input> { 207 + type Item = Result<Token<'input>, Error>; 208 + 209 + fn next(&mut self) -> Option<Self::Item> { 210 + #[allow(clippy::never_loop)] 211 + loop { 212 + // two subsequent char tokens. 213 + if let Some((start, a, b)) = self.two() { 214 + let two = match (a, b) { 215 + ('~', '>') => Some(Pessimistic), 216 + ('!', '=') => Some(NotEq), 217 + ('<', '=') => Some(LtEq), 218 + ('>', '=') => Some(GtEq), 219 + ('=', '=') => Some(Eq), 220 + ('o', 'r') => Some(Or), 221 + ('a', 'n') => { 222 + self.step_n(2); 223 + return Some(self.and(start)); 224 + } 225 + _ => None, 226 + }; 227 + 228 + if let Some(two) = two { 229 + self.step_n(2); 230 + return Some(Ok(two)); 231 + } 232 + } 233 + 234 + // single char and start of numeric tokens. 235 + if let Some((start, c)) = self.one() { 236 + let tok = match c { 237 + ' ' | '\t' | '\n' | '\r' => { 238 + self.step(); 239 + return Some(self.whitespace(start)); 240 + } 241 + '>' => Gt, 242 + '<' => Lt, 243 + '.' => Dot, 244 + '-' => Hyphen, 245 + '+' => Plus, 246 + '0'..='9' | 'a'..='z' | 'A'..='Z' => { 247 + self.step(); 248 + return Some(self.component(start)); 249 + } 250 + c => return Some(Err(UnexpectedChar(c))), 251 + }; 252 + 253 + self.step(); 254 + return Some(Ok(tok)); 255 + }; 256 + 257 + return None; 258 + } 259 + } 260 + } 261 + 262 + #[cfg(test)] 263 + mod tests { 264 + use super::*; 265 + 266 + fn lex(input: &str) -> Vec<Token<'_>> { 267 + Lexer::new(input).map(Result::unwrap).collect::<Vec<_>>() 268 + } 269 + 270 + #[test] 271 + pub fn simple_tokens() { 272 + assert_eq!( 273 + lex("!===><<=>=~>.-+orand"), 274 + vec![ 275 + NotEq, 276 + Eq, 277 + Gt, 278 + Lt, 279 + LtEq, 280 + GtEq, 281 + Pessimistic, 282 + Dot, 283 + Hyphen, 284 + Plus, 285 + Or, 286 + And 287 + ] 288 + ); 289 + } 290 + 291 + #[test] 292 + pub fn whitespace() { 293 + assert_eq!( 294 + lex(" foo \t\n\rbar"), 295 + vec![ 296 + Whitespace(0, 2), 297 + AlphaNumeric("foo"), 298 + Whitespace(5, 9), 299 + AlphaNumeric("bar"), 300 + ] 301 + ); 302 + } 303 + 304 + #[test] 305 + pub fn components() { 306 + assert_eq!(lex("42"), vec![Numeric(42)]); 307 + assert_eq!(lex("0"), vec![Numeric(0)]); 308 + assert_eq!(lex("5885644aa"), vec![AlphaNumeric("5885644aa")]); 309 + assert_eq!(lex("beta2"), vec![AlphaNumeric("beta2")]); 310 + assert_eq!(lex("beta.2"), vec![AlphaNumeric("beta"), Dot, Numeric(2)]); 311 + } 312 + 313 + #[test] 314 + pub fn empty() { 315 + assert_eq!(lex(""), vec![]); 316 + } 317 + 318 + #[test] 319 + pub fn numeric_all_numbers() { 320 + let expected: Vec<Token> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 321 + .into_iter() 322 + .map(Numeric) 323 + .collect::<Vec<_>>(); 324 + 325 + let actual: Vec<_> = lex("0 1 2 3 4 5 6 7 8 9") 326 + .into_iter() 327 + .filter(|t| !t.is_whitespace()) 328 + .collect(); 329 + 330 + assert_eq!(actual, expected); 331 + } 332 + }
+409
hexpm/src/version/parser.rs
··· 1 + // Based off of https://github.com/steveklabnik/semver-parser/blob/bee9de80aaa9653c5eb46a83658606cb21151e65/src/parser.rs 2 + 3 + use std::fmt; 4 + use std::mem; 5 + 6 + use self::Error::*; 7 + use super::lexer::{self, Lexer, Token}; 8 + use crate::version::{Identifier, Version}; 9 + use thiserror::Error; 10 + 11 + type PubgrubRange = pubgrub::Range<Version>; 12 + 13 + #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Error)] 14 + pub enum Error { 15 + /// Needed more tokens for parsing, but none are available. 16 + UnexpectedEnd, 17 + /// Unexpected token. 18 + UnexpectedToken(String), 19 + /// An error occurred in the lexer. 20 + Lexer(lexer::Error), 21 + /// More input available. 22 + MoreInput(String), 23 + /// Encountered empty predicate in a set of predicates. 24 + EmptyPredicate, 25 + /// Encountered an empty range. 26 + EmptyRange, 27 + /// Encountered a semver that's missing the minor and patch version. 28 + MinorVersionMissing(u32), 29 + /// Encountered a semver that's missing the patch version. 30 + PatchVersionMissing(u32, u32), 31 + } 32 + 33 + impl From<lexer::Error> for Error { 34 + fn from(value: lexer::Error) -> Self { 35 + Error::Lexer(value) 36 + } 37 + } 38 + 39 + impl fmt::Display for Error { 40 + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 41 + use self::Error::*; 42 + 43 + match *self { 44 + UnexpectedEnd => write!(fmt, "expected more input"), 45 + UnexpectedToken(ref token) => write!(fmt, "encountered unexpected token: {:?}", token), 46 + Lexer(ref error) => write!(fmt, "lexer error: {:?}", error), 47 + MoreInput(ref tokens) => write!(fmt, "expected end of input, but got: {:?}", tokens), 48 + EmptyPredicate => write!(fmt, "encountered empty predicate"), 49 + EmptyRange => write!(fmt, "encountered empty range"), 50 + MinorVersionMissing(major) => { 51 + write!(fmt, "missing minor and patch versions: {:?}", major) 52 + } 53 + PatchVersionMissing(major, minor) => { 54 + write!(fmt, "missing patch version: {:?}.{:?}", major, minor) 55 + } 56 + } 57 + } 58 + } 59 + 60 + /// impl for backwards compatibility. 61 + impl From<Error> for String { 62 + fn from(value: Error) -> Self { 63 + value.to_string() 64 + } 65 + } 66 + 67 + /// A recursive-descent parser for parsing version requirements. 68 + pub struct Parser<'input> { 69 + /// Source of token. 70 + lexer: Lexer<'input>, 71 + /// Lookaehead. 72 + c1: Option<Token<'input>>, 73 + } 74 + 75 + impl<'input> Parser<'input> { 76 + /// Construct a new parser for the given input. 77 + pub fn new(input: &'input str) -> Result<Parser<'input>, Error> { 78 + let mut lexer = Lexer::new(input); 79 + 80 + let c1 = if let Some(c1) = lexer.next() { 81 + Some(c1?) 82 + } else { 83 + None 84 + }; 85 + 86 + Ok(Parser { lexer, c1 }) 87 + } 88 + 89 + /// Pop one token. 90 + #[inline(always)] 91 + fn pop(&mut self) -> Result<Token<'input>, Error> { 92 + let c1 = if let Some(c1) = self.lexer.next() { 93 + Some(c1?) 94 + } else { 95 + None 96 + }; 97 + 98 + mem::replace(&mut self.c1, c1).ok_or(UnexpectedEnd) 99 + } 100 + 101 + /// Peek one token. 102 + #[inline(always)] 103 + fn peek(&mut self) -> Option<&Token<'input>> { 104 + self.c1.as_ref() 105 + } 106 + 107 + /// Skip whitespace if present. 108 + fn skip_whitespace(&mut self) -> Result<(), Error> { 109 + match self.peek() { 110 + Some(&Token::Whitespace(_, _)) => self.pop().map(|_| ()), 111 + _ => Ok(()), 112 + } 113 + } 114 + 115 + /// Check that some whitespace is next and then discard it 116 + fn expect_whitespace(&mut self) -> Result<(), Error> { 117 + match self.pop()? { 118 + Token::Whitespace(_, _) => Ok(()), 119 + token => Err(UnexpectedToken(token.to_string())), 120 + } 121 + } 122 + 123 + /// Parse a single numeric. 124 + pub fn numeric(&mut self) -> Result<u32, Error> { 125 + match self.pop()? { 126 + Token::Numeric(number) => Ok(number), 127 + token => Err(UnexpectedToken(token.to_string())), 128 + } 129 + } 130 + 131 + fn dot(&mut self) -> Result<(), Error> { 132 + match self.pop()? { 133 + Token::Dot => Ok(()), 134 + token => Err(UnexpectedToken(token.to_string())), 135 + } 136 + } 137 + 138 + /// Parse a dot, then a numeric. 139 + fn dot_numeric(&mut self) -> Result<u32, Error> { 140 + self.dot()?; 141 + self.numeric() 142 + } 143 + 144 + /// Parse an string identifier. 145 + /// 146 + /// Like, `foo`, or `bar`, or `beta-1`. 147 + pub fn identifier(&mut self) -> Result<Identifier, Error> { 148 + let identifier = match self.pop()? { 149 + Token::AlphaNumeric(identifier) => { 150 + // TODO: Borrow? 151 + Identifier::AlphaNumeric(identifier.to_string()) 152 + } 153 + Token::Numeric(n) => Identifier::Numeric(n), 154 + tok => return Err(UnexpectedToken(tok.to_string())), 155 + }; 156 + 157 + if let Some(&Token::Hyphen) = self.peek() { 158 + // pop the peeked hyphen 159 + self.pop()?; 160 + // concat with any following identifiers 161 + Ok(identifier 162 + .concat("-") 163 + .concat(&self.identifier()?.to_string())) 164 + } else { 165 + Ok(identifier) 166 + } 167 + } 168 + 169 + /// Parse all pre-release identifiers, separated by dots. 170 + /// 171 + /// Like, `abcdef.1234`. 172 + fn pre(&mut self) -> Result<Vec<Identifier>, Error> { 173 + match self.peek() { 174 + Some(&Token::Hyphen) => {} 175 + _ => return Ok(vec![]), 176 + } 177 + 178 + // pop the peeked hyphen. 179 + self.pop()?; 180 + self.parts() 181 + } 182 + 183 + /// Parse a dot-separated set of identifiers. 184 + fn parts(&mut self) -> Result<Vec<Identifier>, Error> { 185 + let mut parts = Vec::new(); 186 + 187 + parts.push(self.identifier()?); 188 + 189 + while let Some(&Token::Dot) = self.peek() { 190 + self.pop()?; 191 + 192 + parts.push(self.identifier()?); 193 + } 194 + 195 + Ok(parts) 196 + } 197 + 198 + /// Parse optional build metadata. 199 + /// 200 + /// Like, `` (empty), or `+abcdef`. 201 + fn plus_build_metadata(&mut self) -> Result<Option<String>, Error> { 202 + match self.peek() { 203 + Some(&Token::Plus) => self.pop()?, 204 + _ => return Ok(None), 205 + }; 206 + 207 + let mut buffer = String::new(); 208 + 209 + loop { 210 + match self.pop() { 211 + Err(UnexpectedEnd) => break, 212 + Ok(Token::LeadingZero(s)) => buffer.push_str(s), 213 + Ok(Token::AlphaNumeric(s)) => buffer.push_str(s), 214 + Ok(Token::Numeric(s)) => buffer.push_str(&s.to_string()), 215 + Ok(Token::Dot) => buffer.push('.'), 216 + Ok(token) => return Err(UnexpectedToken(token.to_string())), 217 + Err(error) => return Err(error), 218 + } 219 + } 220 + 221 + if buffer.is_empty() { 222 + Err(UnexpectedEnd) 223 + } else { 224 + Ok(Some(buffer)) 225 + } 226 + } 227 + 228 + /// Parse a version. 229 + /// 230 + /// Like, `1.0.0` or `3.0.0-beta.1`. 231 + pub fn version(&mut self) -> Result<Version, Error> { 232 + self.skip_whitespace()?; 233 + 234 + let major = self.numeric()?; 235 + let minor = self 236 + .dot_numeric() 237 + .map_err(|_| Error::MinorVersionMissing(major))?; 238 + let patch = self 239 + .dot_numeric() 240 + .map_err(|_| Error::PatchVersionMissing(major, minor))?; 241 + let pre = self.pre()?; 242 + let build = self.plus_build_metadata()?; 243 + 244 + self.skip_whitespace()?; 245 + 246 + Ok(Version { 247 + major, 248 + minor, 249 + patch, 250 + pre, 251 + build, 252 + }) 253 + } 254 + 255 + /// Parse a version range requirement. 256 + /// 257 + /// Like, `~> 1.0.0` or `3.0.0-beta.1 or < 1.0 and > 0.2.3`. 258 + pub fn range(&mut self) -> Result<PubgrubRange, Error> { 259 + let mut range: Option<PubgrubRange> = None; 260 + 261 + loop { 262 + let constraint = self.range_ands_section()?; 263 + range = Some(match range { 264 + None => constraint, 265 + Some(range) => range.union(&constraint), 266 + }); 267 + if self.peek() == Some(&Token::Or) { 268 + self.pop()?; 269 + self.expect_whitespace()?; 270 + } else { 271 + break; 272 + } 273 + } 274 + 275 + self.skip_whitespace()?; 276 + range.ok_or(UnexpectedEnd) 277 + } 278 + 279 + fn pessimistic_version_constraint(&mut self) -> Result<PubgrubRange, Error> { 280 + let mut included_patch = false; 281 + let major = self.numeric()?; 282 + let minor = self.dot_numeric()?; 283 + let patch = match self.peek() { 284 + Some(Token::Dot) => { 285 + included_patch = true; 286 + self.pop()?; 287 + self.numeric()? 288 + } 289 + _ => 0, 290 + }; 291 + let pre = self.pre()?; 292 + let build = self.plus_build_metadata()?; 293 + 294 + let lower = Version { 295 + major, 296 + minor, 297 + patch, 298 + pre, 299 + build, 300 + }; 301 + let upper = if included_patch { 302 + lower.bump_minor() 303 + } else { 304 + lower.bump_major() 305 + }; 306 + Ok( 307 + PubgrubRange::higher_than(lower) 308 + .intersection(&PubgrubRange::strictly_lower_than(upper)), 309 + ) 310 + } 311 + 312 + fn range_ands_section(&mut self) -> Result<PubgrubRange, Error> { 313 + use Token::*; 314 + let mut range = None; 315 + let and = |range: Option<PubgrubRange>, constraint: PubgrubRange| { 316 + Some(match range { 317 + None => constraint, 318 + Some(range) => range.intersection(&constraint), 319 + }) 320 + }; 321 + loop { 322 + self.skip_whitespace()?; 323 + match self.peek() { 324 + None => break, 325 + Some(Numeric(_)) => range = and(range, PubgrubRange::singleton(self.version()?)), 326 + 327 + Some(Eq) => { 328 + self.pop()?; 329 + range = and(range, PubgrubRange::singleton(self.version()?)); 330 + } 331 + 332 + Some(NotEq) => { 333 + self.pop()?; 334 + let version = self.version()?; 335 + let bumped = version.bump_patch(); 336 + let below = PubgrubRange::strictly_lower_than(version); 337 + let above = PubgrubRange::higher_than(bumped); 338 + range = and(range, below.union(&above)); 339 + } 340 + 341 + Some(Gt) => { 342 + self.pop()?; 343 + range = and( 344 + range, 345 + PubgrubRange::higher_than(self.version()?.bump_patch()), 346 + ); 347 + } 348 + 349 + Some(GtEq) => { 350 + self.pop()?; 351 + range = and(range, PubgrubRange::higher_than(self.version()?)); 352 + } 353 + 354 + Some(Lt) => { 355 + self.pop()?; 356 + range = and(range, PubgrubRange::strictly_lower_than(self.version()?)); 357 + } 358 + 359 + Some(LtEq) => { 360 + self.pop()?; 361 + range = and( 362 + range, 363 + PubgrubRange::strictly_lower_than(self.version()?.bump_patch()), 364 + ); 365 + } 366 + 367 + Some(Pessimistic) => { 368 + self.pop()?; 369 + self.skip_whitespace()?; 370 + range = and(range, self.pessimistic_version_constraint()?); 371 + } 372 + 373 + Some(_) => return Err(UnexpectedToken(self.pop()?.to_string())), 374 + }; 375 + 376 + self.skip_whitespace()?; 377 + if self.peek() == Some(&Token::And) { 378 + self.pop()?; 379 + self.expect_whitespace()?; 380 + } else { 381 + break; 382 + } 383 + } 384 + self.skip_whitespace()?; 385 + range.ok_or(UnexpectedEnd) 386 + } 387 + 388 + /// Check if we have reached the end of input. 389 + pub fn is_eof(&mut self) -> bool { 390 + self.c1.is_none() 391 + } 392 + 393 + /// Get the rest of the tokens in the parser. 394 + /// 395 + /// Useful for debugging. 396 + pub fn tail(&mut self) -> Result<Vec<Token<'input>>, Error> { 397 + let mut out = Vec::new(); 398 + 399 + if let Some(t) = self.c1.take() { 400 + out.push(t); 401 + } 402 + 403 + for t in self.lexer.by_ref() { 404 + out.push(t?); 405 + } 406 + 407 + Ok(out) 408 + } 409 + }
+471
hexpm/src/version/tests.rs
··· 1 + use std::cmp::Ordering::{Equal, Greater, Less}; 2 + use std::collections::HashMap; 3 + 4 + use parser::Error; 5 + 6 + use super::{ 7 + Identifier::{AlphaNumeric, Numeric}, 8 + *, 9 + }; 10 + 11 + // Tests adapted from the tests for Elixir's version module 12 + 13 + macro_rules! version_parse_test { 14 + ($name:ident, $input:expr, $major:expr, $minor:expr, $patch:expr,$pre:expr, $build:expr) => { 15 + #[test] 16 + fn $name() { 17 + assert_eq!( 18 + Version::parse($input).unwrap(), 19 + Version { 20 + major: $major, 21 + minor: $minor, 22 + patch: $patch, 23 + pre: $pre, 24 + build: $build 25 + } 26 + ); 27 + } 28 + }; 29 + 30 + ($name:ident, $input:expr, $major:expr, $minor:expr, $patch:expr, $pre:expr) => { 31 + #[test] 32 + fn $name() { 33 + assert_eq!( 34 + Version::parse($input).unwrap(), 35 + Version { 36 + major: $major, 37 + minor: $minor, 38 + patch: $patch, 39 + pre: $pre, 40 + build: None 41 + } 42 + ); 43 + } 44 + }; 45 + 46 + ($name:ident, $input:expr, $major:expr, $minor:expr, $patch:expr) => { 47 + #[test] 48 + fn $name() { 49 + assert_eq!( 50 + Version::parse($input).unwrap(), 51 + Version { 52 + major: $major, 53 + minor: $minor, 54 + patch: $patch, 55 + pre: vec![], 56 + build: None 57 + } 58 + ); 59 + } 60 + }; 61 + } 62 + 63 + macro_rules! version_parse_fail_test { 64 + ($name:ident, $input:expr) => { 65 + #[test] 66 + fn $name() { 67 + println!("{}", $input); 68 + Version::parse($input).unwrap_err(); 69 + } 70 + }; 71 + } 72 + 73 + macro_rules! version_parse_print { 74 + ($name:ident, $input:expr) => { 75 + #[test] 76 + fn $name() { 77 + assert_eq!($input, Version::parse($input).unwrap().to_string().as_str()); 78 + } 79 + }; 80 + } 81 + 82 + version_parse_test!(triplet, "1.2.3", 1, 2, 3); 83 + 84 + version_parse_test!( 85 + build, 86 + "1.4.5+ignore", 87 + 1, 88 + 4, 89 + 5, 90 + vec![], 91 + Some("ignore".to_string()) 92 + ); 93 + 94 + version_parse_test!( 95 + two_part_build, 96 + "0.0.1+sha.0702245", 97 + 0, 98 + 0, 99 + 1, 100 + vec![], 101 + Some("sha.0702245".to_string()) 102 + ); 103 + 104 + version_parse_test!( 105 + pre, 106 + "1.4.5-6-g3318bd5", 107 + 1, 108 + 4, 109 + 5, 110 + vec![AlphaNumeric("6-g3318bd5".to_string())] 111 + ); 112 + 113 + version_parse_test!( 114 + multi_part_pre, 115 + "1.4.5-6.7.eight", 116 + 1, 117 + 4, 118 + 5, 119 + vec![Numeric(6), Numeric(7), AlphaNumeric("eight".to_string())] 120 + ); 121 + 122 + version_parse_test!( 123 + pre_and_build, 124 + "1.4.5-6-g3318bd5+ignore", 125 + 1, 126 + 4, 127 + 5, 128 + vec![AlphaNumeric("6-g3318bd5".to_string())], 129 + Some("ignore".to_string()) 130 + ); 131 + 132 + version_parse_fail_test!(just_a_word, "foobar"); 133 + 134 + version_parse_fail_test!(just_major, "2"); 135 + 136 + version_parse_fail_test!(major_dor, "2."); 137 + 138 + version_parse_fail_test!(major_minor, "2.3"); 139 + 140 + version_parse_fail_test!(major_minor_dot, "2.3."); 141 + 142 + version_parse_fail_test!(triplet_dash, "2.3.0-"); 143 + 144 + version_parse_fail_test!(triplet_plus, "2.3.0+"); 145 + 146 + version_parse_fail_test!(triplet_dot, "2.3.0."); 147 + 148 + version_parse_fail_test!(quad, "2.3.0.4"); 149 + 150 + version_parse_fail_test!(missing_minor, "2.3.-rc.1"); 151 + 152 + version_parse_fail_test!(missing_minor_with_dot, "2.3.+rc.1"); 153 + 154 + version_parse_fail_test!(zero_pre, "2.3.0-01"); 155 + 156 + version_parse_fail_test!(double_zero_pre, "2.3.00-1"); 157 + 158 + version_parse_fail_test!(double_zero, "2.3.00"); 159 + 160 + version_parse_fail_test!(leading_zero_minor, "2.03.0"); 161 + 162 + version_parse_fail_test!(leading_zero_major, "02.3.0"); 163 + 164 + version_parse_fail_test!(extra_whitespace, "0. 0.0"); 165 + 166 + version_parse_fail_test!(and_in_version, "0.1.0-andpre"); 167 + 168 + version_parse_print!(print_triplet, "1.100.1000"); 169 + 170 + version_parse_print!(print_pre, "1.100.4-dev"); 171 + 172 + version_parse_print!(print_pre_dot, "1.100.4-dev.1.r.t"); 173 + 174 + version_parse_print!(print_build, "1.100.4+dev.1.r.t"); 175 + 176 + version_parse_print!(print_pre_build, "1.100.4-ewfjhwefj.wefw.w.1.ff+dev.1.r.t"); 177 + 178 + macro_rules! parse_range_test { 179 + ($name:ident, $input:expr, $expected:expr) => { 180 + #[test] 181 + fn $name() { 182 + assert_eq!(Version::parse_range($input).unwrap(), $expected); 183 + } 184 + }; 185 + } 186 + 187 + macro_rules! parse_range_fail_test { 188 + ($name:ident, $input:expr) => { 189 + #[test] 190 + fn $name() { 191 + Version::parse_range($input).unwrap_err(); 192 + } 193 + }; 194 + } 195 + 196 + fn v(a: u32, b: u32, c: u32) -> Version { 197 + Version::new(a, b, c) 198 + } 199 + 200 + fn v_(major: u32, minor: u32, patch: u32, pre: Vec<Identifier>, build: Option<String>) -> Version { 201 + Version { 202 + major, 203 + minor, 204 + patch, 205 + pre, 206 + build, 207 + } 208 + } 209 + 210 + type PubgrubRange = pubgrub::Range<Version>; 211 + 212 + parse_range_test!(leading_space, " 1.2.3", PubgrubRange::singleton(v(1, 2, 3))); 213 + parse_range_test!( 214 + trailing_space, 215 + "1.2.3 ", 216 + PubgrubRange::singleton(v(1, 2, 3)) 217 + ); 218 + 219 + parse_range_test!(eq_triplet, "== 1.2.3 ", PubgrubRange::singleton(v(1, 2, 3))); 220 + 221 + parse_range_test!( 222 + eq_triplet_nospace, 223 + "==1.2.3 ", 224 + PubgrubRange::singleton(v(1, 2, 3)) 225 + ); 226 + 227 + parse_range_test!( 228 + neq_triplet, 229 + "!= 1.2.3", 230 + PubgrubRange::strictly_lower_than(v(1, 2, 3)).union(&PubgrubRange::higher_than(v(1, 2, 4))) 231 + ); 232 + 233 + parse_range_test!(implicit_eq, "2.2.3", PubgrubRange::singleton(v(2, 2, 3))); 234 + 235 + parse_range_test!( 236 + range_pre_build, 237 + "1.2.3-thing+oop", 238 + PubgrubRange::singleton(v_( 239 + 1, 240 + 2, 241 + 3, 242 + vec![Identifier::AlphaNumeric("thing".to_string())], 243 + Some("oop".to_string()) 244 + )) 245 + ); 246 + 247 + parse_range_test!( 248 + and, 249 + "< 1.2.3 and > 1.0.1", 250 + PubgrubRange::strictly_lower_than(v(1, 2, 3)) 251 + .intersection(&PubgrubRange::higher_than(v(1, 0, 2))) 252 + ); 253 + 254 + parse_range_test!( 255 + or, 256 + "< 1.2.3 or > 1.0.1", 257 + PubgrubRange::strictly_lower_than(v(1, 2, 3)).union(&PubgrubRange::higher_than(v(1, 0, 2))) 258 + ); 259 + 260 + parse_range_test!(gt, "> 1.0.0", PubgrubRange::higher_than(v(1, 0, 1))); 261 + parse_range_test!(gt_eq, ">= 1.0.0", PubgrubRange::higher_than(v(1, 0, 0))); 262 + parse_range_test!(lt, "< 1.0.0", PubgrubRange::strictly_lower_than(v(1, 0, 0))); 263 + parse_range_test!( 264 + lt_eq, 265 + "<= 1.0.0", 266 + PubgrubRange::strictly_lower_than(v(1, 0, 1)) 267 + ); 268 + 269 + parse_range_test!( 270 + pessimistic_pair, 271 + "~> 2.2", 272 + PubgrubRange::higher_than(v(2, 2, 0)) 273 + .intersection(&PubgrubRange::strictly_lower_than(v(3, 0, 0))) 274 + ); 275 + 276 + parse_range_test!( 277 + pessimistic_triplet, 278 + "~> 4.6.5", 279 + PubgrubRange::higher_than(v(4, 6, 5)) 280 + .intersection(&PubgrubRange::strictly_lower_than(v(4, 7, 0))) 281 + ); 282 + 283 + parse_range_test!( 284 + pessimistic_triplet_pre, 285 + "~> 4.6.5-eee", 286 + PubgrubRange::higher_than(v_( 287 + 4, 288 + 6, 289 + 5, 290 + vec![Identifier::AlphaNumeric("eee".to_string())], 291 + None, 292 + )) 293 + .intersection(&PubgrubRange::strictly_lower_than(v(4, 7, 0))) 294 + ); 295 + 296 + parse_range_test!( 297 + pessimistic_triplet_build, 298 + "~> 4.6.5+eee", 299 + PubgrubRange::higher_than(v_(4, 6, 5, vec![], Some("eee".to_string()))) 300 + .intersection(&PubgrubRange::strictly_lower_than(v(4, 7, 0))) 301 + ); 302 + 303 + parse_range_test!( 304 + greater_or_pessimistic, 305 + "> 10.0.0 or ~> 3.0.0", 306 + PubgrubRange::higher_than(v(10, 0, 1)).union( 307 + &PubgrubRange::higher_than(v(3, 0, 0)) 308 + .intersection(&PubgrubRange::strictly_lower_than(v(3, 1, 0))) 309 + ) 310 + ); 311 + 312 + parse_range_test!( 313 + pessimistic_or_pessimistic, 314 + "~> 1.0.0 or ~> 3.0.0", 315 + PubgrubRange::higher_than(v(1, 0, 0)) 316 + .intersection(&PubgrubRange::strictly_lower_than(v(1, 1, 0))) 317 + .union( 318 + &PubgrubRange::higher_than(v(3, 0, 0)) 319 + .intersection(&PubgrubRange::strictly_lower_than(v(3, 1, 0))) 320 + ) 321 + ); 322 + 323 + parse_range_test!( 324 + pessimistic_and_gt, 325 + "~> 0.6 and >= 0.6.16", 326 + PubgrubRange::higher_than(v(0, 6, 0)) 327 + .intersection(&PubgrubRange::strictly_lower_than(v(1, 0, 0))) 328 + .intersection(&PubgrubRange::higher_than(v(0, 6, 16))) 329 + ); 330 + 331 + parse_range_test!( 332 + pessimistic_and_gt_pre, 333 + "~> 1.0-pre and >= 1.0.0-pre.5", 334 + PubgrubRange::higher_than(v_( 335 + 1, 336 + 0, 337 + 0, 338 + vec![Identifier::AlphaNumeric("pre".to_string())], 339 + None 340 + )) 341 + .intersection(&PubgrubRange::strictly_lower_than(v(2, 0, 0))) 342 + .intersection(&PubgrubRange::higher_than(v_( 343 + 1, 344 + 0, 345 + 0, 346 + vec![ 347 + Identifier::AlphaNumeric("pre".to_string()), 348 + Identifier::Numeric(5) 349 + ], 350 + None 351 + ))) 352 + ); 353 + 354 + parse_range_fail_test!(range_quad, "1.1.1.1"); 355 + parse_range_fail_test!(range_just_major, "1"); 356 + parse_range_fail_test!(range_just_major_minor, "1.1"); 357 + parse_range_fail_test!(alpha_component, "1.1.a"); 358 + 359 + parse_range_fail_test!(range_word, "foobar"); 360 + parse_range_fail_test!(range_major_dot, "2."); 361 + parse_range_fail_test!(range_major_minor_dot, "2.3."); 362 + parse_range_fail_test!(range_triplet_dash, "2.3.0-"); 363 + parse_range_fail_test!(range_triplet_plus, "2.3.0+"); 364 + parse_range_fail_test!(range_triplet_dot, "2.3.0."); 365 + parse_range_fail_test!(range_dot_dash, "2.3.-rc.1"); 366 + parse_range_fail_test!(range_dot_plus, "2.3.+rc.1"); 367 + parse_range_fail_test!(range_zero_pre, "2.3.0-01"); 368 + parse_range_fail_test!(patch_zerozero_dash, "2.3.00-1"); 369 + parse_range_fail_test!(patch_zerozero, "2.3.00"); 370 + parse_range_fail_test!(minor_leading_zero, "2.03.0"); 371 + parse_range_fail_test!(major_leading_zero, "02.3.0"); 372 + parse_range_fail_test!(triplet_containing_whitespace, "0. 0.0"); 373 + parse_range_fail_test!(dash_amp, "0.1.0-&&pre"); 374 + 375 + parse_range_fail_test!(or_whitespace_before, "!= 1.2.3or == 1.0.1"); 376 + parse_range_fail_test!(or_whitespace_after, "!= 1.2.3 or== 1.0.1"); 377 + parse_range_fail_test!(and_whitespace_before, "!= 1.2.3and == 1.0.1"); 378 + parse_range_fail_test!(and_whitespace_after, "!= 1.2.3 and== 1.0.1"); 379 + 380 + parse_range_fail_test!(trailing_and, "1.1.1 and"); 381 + parse_range_fail_test!(trailing_or, "1.1.1 or"); 382 + parse_range_fail_test!(leading_and, "and 1.1.1"); 383 + parse_range_fail_test!(leading_or, "and 1.1.1"); 384 + parse_range_fail_test!(just_and, "and"); 385 + parse_range_fail_test!(just_or, "and"); 386 + 387 + parse_range_fail_test!(duplicate_eq, "== =="); 388 + parse_range_fail_test!(just_eq, "=="); 389 + parse_range_fail_test!(empty, ""); 390 + 391 + parse_range_fail_test!(pessimistic_major, "~> 1"); 392 + 393 + macro_rules! assert_order { 394 + ($name:ident, $left:expr, $ord:expr, $right:expr) => { 395 + #[test] 396 + fn $name() { 397 + let left = Version::parse($left); 398 + let right = Version::parse($right); 399 + assert_eq!(left.cmp(&right), $ord) 400 + } 401 + }; 402 + } 403 + 404 + assert_order!(ord_same, "1.0.0", Equal, "1.0.0"); 405 + assert_order!(ord_same_build_right, "1.0.0", Equal, "1.0.0+1"); 406 + assert_order!(ord_same_build_left, "1.0.0+1", Equal, "1.0.0"); 407 + assert_order!(ord_same_diff_build, "1.0.0+1", Equal, "1.0.0+2"); 408 + 409 + assert_order!(ord_diff_build, "1.0.0+2", Equal, "1.0.0+1"); 410 + 411 + assert_order!(ord_major_greater, "3.0.0", Greater, "2.0.0"); 412 + assert_order!(ord_major_lesser, "1.0.0", Less, "2.0.0"); 413 + 414 + assert_order!(ord_minor_greater, "1.1.0", Greater, "1.0.0"); 415 + assert_order!(ord_minor_lesser, "1.0.0", Less, "1.1.0"); 416 + 417 + assert_order!(ord_patch_greater, "1.0.1", Greater, "1.0.0"); 418 + assert_order!(ord_patch_lesser, "1.0.0", Less, "1.0.1"); 419 + 420 + assert_order!(ord_pre_smaller_than_zero, "1.0.0", Greater, "1.0.0-rc1"); 421 + assert_order!(ord_pre_smaller_than_zero_flip, "1.0.0-rc1", Less, "1.0.0"); 422 + 423 + assert_order!(ord_pre_rc1_2, "1.0.0-rc1", Less, "1.0.0-rc2"); 424 + 425 + #[test] 426 + fn manifest_toml() { 427 + let manifest = toml::to_string( 428 + &vec![ 429 + ( 430 + "gleam_stdlib".to_string(), 431 + Version { 432 + major: 0, 433 + minor: 17, 434 + patch: 1, 435 + pre: vec![], 436 + build: None, 437 + }, 438 + ), 439 + ( 440 + "thingy".to_string(), 441 + Version { 442 + major: 0, 443 + minor: 1, 444 + patch: 0, 445 + pre: vec![], 446 + build: None, 447 + }, 448 + ), 449 + ] 450 + .into_iter() 451 + .collect::<HashMap<String, Version>>(), 452 + ) 453 + .unwrap(); 454 + let expected1 = r#"thingy = "0.1.0" 455 + gleam_stdlib = "0.17.1" 456 + "#; 457 + let expected2 = r#"gleam_stdlib = "0.17.1" 458 + thingy = "0.1.0" 459 + "#; 460 + assert!(manifest == expected1 || manifest == expected2); 461 + } 462 + 463 + #[test] 464 + fn missing_minor_has_correct_error_type() { 465 + assert_eq!(Version::parse("1"), Err(Error::MinorVersionMissing(1))) 466 + } 467 + 468 + #[test] 469 + fn missing_patch_has_correct_error_type() { 470 + assert_eq!(Version::parse("1.2"), Err(Error::PatchVersionMissing(1, 2))) 471 + }
hexpm/test/example.tar

This is a binary file and will not be displayed.

hexpm/test/example.tar.gz

This is a binary file and will not be displayed.

hexpm/test/gleam_stdlib-0.14.0.tar

This is a binary file and will not be displayed.

hexpm/test/package_exfmt

This is a binary file and will not be displayed.

+9
hexpm/test/public_key
··· 1 + -----BEGIN PUBLIC KEY----- 2 + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApqREcFDt5vV21JVe2QNB 3 + Edvzk6w36aNFhVGWN5toNJRjRJ6m4hIuG4KaXtDWVLjnvct6MYMfqhC79HAGwyF+ 4 + IqR6Q6a5bbFSsImgBJwz1oadoVKD6ZNetAuCIK84cjMrEFRkELtEIPNHblCzUkkM 5 + 3rS9+DPlnfG8hBvGi6tvQIuZmXGCxF/73hU0/MyGhbmEjIKRtG6b0sJYKelRLTPW 6 + XgK7s5pESgiwf2YC/2MGDXjAJfpfCd0RpLdvd4eRiXtVlE9qO9bND94E7PgQ/xqZ 7 + J1i2xWFndWa6nfFnRxZmCStCOZWYYPlaxr+FZceFbpMwzTNs4g3d4tLNUcbKAIH4 8 + 0wIDAQAB 9 + -----END PUBLIC KEY-----
hexpm/test/versions

This is a binary file and will not be displayed.

hexpm/versions

This is a binary file and will not be displayed.