diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..a766f4e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: test + +on: + push: + branches: + - master + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: "26.0.2" + gleam-version: "1.4.0" + rebar3-version: "3" + # elixir-version: "1.15.4" + - run: gleam deps download + - run: gleam test + - run: gleam format --check src test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..599be4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..994a965 --- /dev/null +++ b/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2023, Pierre Golfier . + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7266cce --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# releam + +Releam is a gleam package release CLI tool and also a set of utilities for parsing conventional commits + +[![Package Version](https://img.shields.io/hexpm/v/releam)](https://hex.pm/packages/releam) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/releam/) + +```sh +gleam add releam +``` +```gleam +import releam + +pub fn main() { + // TODO: An example of the project in use +} +``` + +Further documentation can be found at . + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +``` diff --git a/gleam.toml b/gleam.toml new file mode 100644 index 0000000..e7947ac --- /dev/null +++ b/gleam.toml @@ -0,0 +1,20 @@ +name = "releam" +version = "0.1.0" + +# Fill out these fields if you intend to generate HTML documentation or publish +# your project to the Hex package manager. +# +description = "Releam is a gleam package release CLI tool and also a set of utilities for parsing conventional commits" +licences = ["Apache-2.0"] +repository = { type = "github", user = "pedraal", repo = "releam" } +# links = [{ title = "Website", href = "" }] +# +# For a full reference of all the available options, you can have a look at +# https://gleam.run/writing-gleam/gleam-toml/. + +[dependencies] +gleam_stdlib = ">= 0.34.0 and < 2.0.0" +shellout = ">= 1.6.0 and < 2.0.0" + +[dev-dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml new file mode 100644 index 0000000..302fd22 --- /dev/null +++ b/manifest.toml @@ -0,0 +1,13 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "gleam_stdlib", version = "0.39.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "2D7DE885A6EA7F1D5015D1698920C9BAF7241102836CE0C3837A4F160128A9C4" }, + { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, + { name = "shellout", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "shellout", source = "hex", outer_checksum = "E2FCD18957F0E9F67E1F497FC9FF57393392F8A9BAEAEA4779541DE7A68DD7E0" }, +] + +[requirements] +gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +shellout = { version = ">= 1.6.0 and < 2.0.0" } diff --git a/src/releam.gleam b/src/releam.gleam new file mode 100644 index 0000000..0531d15 --- /dev/null +++ b/src/releam.gleam @@ -0,0 +1,274 @@ +import gleam/io +import gleam/list +import gleam/option.{type Option, None, Some} +import gleam/regex +import gleam/result +import gleam/string +import shellout + +pub type CommitType { + Feat + Fix + Docs + Style + Refactor + Perf + Test + Build + Ci + Chore + Revert +} + +pub type ConventionalCommitParseError { + InvalidCommitType + InvalidCommitDefinition + InvalidCommitMessage +} + +pub type ConventionalFooterParseError { + InvalidConventionalFooter + InvalidConventionalFooterLine +} + +pub type Commit { + Commit( + hash: String, + author: Author, + date: String, + conventional_attributes: Result( + ConventionalAttributes, + ConventionalCommitParseError, + ), + ) +} + +pub type ConventionalAttributes { + ConventionalAttributes( + commit_type: CommitType, + scope: Option(String), + message: String, + body: List(String), + footer: List(#(String, String)), + breaking_change: Bool, + ) +} + +pub type Author { + Author(name: String, email: String) +} + +pub fn main() { + get_last_tag() + |> get_commits_since_last_tag + |> parse_commits + |> io.debug +} + +pub fn get_last_tag() { + shellout.command( + run: "git", + with: ["describe", "--tags", "--abbrev=0"], + in: ".", + opt: [], + ) + |> result.unwrap("") + |> string.replace("\n", "") +} + +pub fn get_commits_since_last_tag(tag: String) { + let reference = case tag { + "" -> "HEAD" + t -> t <> "..HEAD" + } + + shellout.command(run: "git", with: ["log", reference], in: ".", opt: []) + |> result.unwrap("") + |> string.split("\ncommit ") + |> list.map(string.replace(_, "commit ", "")) +} + +pub fn parse_commits(commits: List(String)) { + list.map(commits, parse_commit) + |> list.filter(fn(res) { result.is_ok(res) }) + |> list.map(result.unwrap(_, Commit( + hash: "", + author: Author(name: "", email: ""), + date: "", + conventional_attributes: Error(InvalidCommitDefinition), + ))) +} + +pub fn parse_commit(raw: String) { + let commit_props = + raw + |> string.replace("\n\n", "\n") + |> string.split("\n") + |> list.map(string.trim(_)) + |> list.filter(fn(str) { str != "" }) + case commit_props { + // [hash, author, date, message, ..bodies] -> { + // let conventional_attributes = parse_conventional_attributes(message) + // Ok(Commit( + // hash: hash, + // author: parse_commit_author(author), + // date: parse_commit_date(date), + // conventional_attributes: conventional_attributes, + // )) + // } + _ -> Error(Nil) + } +} + +pub fn parse_commit_author(raw: String) { + let author_props = + string.replace(raw, "Author:", "") + |> string.trim + |> string.replace(">", "") + |> string.split(" <") + + case author_props { + [name, email] -> Author(name: name, email: email) + _ -> Author(name: "", email: "") + } +} + +pub fn parse_commit_date(raw: String) { + string.replace(raw, "Date:", "") + |> string.trim +} + +pub fn parse_conventional_attributes(message: String) { + let sections = + message + |> string.split("\n\n") + |> list.map(string.trim(_)) + |> io.debug + + case sections { + [def] -> parse_conventional_definition(def) + [def, ..rest] -> { + parse_conventional_definition(def) + |> result.map(fn(conventional_attributes) { + let #(body, footer) = parse_conventional_optional_sections(rest) + let is_breaking_change = + list.any(footer, fn(item) { item.0 == "BREAKING CHANGE" }) + + ConventionalAttributes( + ..conventional_attributes, + body: body, + footer: footer, + breaking_change: conventional_attributes.breaking_change + || is_breaking_change, + ) + }) + } + _ -> Error(InvalidCommitMessage) + } +} + +pub fn parse_conventional_definition(def: String) { + let is_breaking_change = string.contains(def, "!:") + let attributes = + def + |> string.replace(")", "") + |> string.replace("!:", ":") + |> string.replace("(", ":") + |> string.split(":") + |> list.map(string.trim(_)) + + case attributes { + [commit_type, scope, message] -> + parse_conventional_commit_type(commit_type) + |> result.map(fn(c_t) { + ConventionalAttributes( + commit_type: c_t, + scope: Some(scope), + message: message, + body: [], + footer: [], + breaking_change: is_breaking_change, + ) + }) + [commit_type, message] -> + parse_conventional_commit_type(commit_type) + |> result.map(fn(c_t) { + ConventionalAttributes( + commit_type: c_t, + scope: None, + message: message, + body: [], + footer: [], + breaking_change: is_breaking_change, + ) + }) + _ -> Error(InvalidCommitDefinition) + } +} + +pub fn parse_conventional_optional_sections(sections: List(String)) { + let footer = + list.last(sections) + |> result.unwrap("") + |> parse_conventional_footer + + case list.reverse(sections), footer { + [_, ..body], Ok(f) -> #(list.reverse(body), f) + body, Error(_) -> #(list.reverse(body), []) + _, _ -> #([], []) + } +} + +pub fn parse_conventional_footer(raw: String) { + let assert Ok(footer_re) = regex.from_string("^[a-zA-Z0-9-]+:") + let assert Ok(breaking_change_footer_re) = + regex.from_string("^BREAKING CHANGE:") + let is_footer = + string.split(raw, "\n") + |> list.map(string.trim(_)) + |> list.all(fn(line) { + regex.check(footer_re, line) + || regex.check(breaking_change_footer_re, line) + }) + + case is_footer { + True -> { + let footer = + raw + |> string.split("\n") + |> list.fold([], fn(footer, line) { + case parse_conventional_footer_line(line) { + Ok(fl) -> [fl, ..footer] + Error(_) -> footer + } + }) + + Ok(list.reverse(footer)) + } + False -> Error(InvalidConventionalFooter) + } +} + +pub fn parse_conventional_footer_line(line: String) { + case string.split(line, ":") |> list.map(string.trim(_)) { + [key, value] -> Ok(#(key, value)) + _ -> Error(InvalidConventionalFooterLine) + } +} + +pub fn parse_conventional_commit_type(commit_type: String) { + case commit_type { + "feat" -> Ok(Feat) + "fix" -> Ok(Fix) + "docs" -> Ok(Docs) + "style" -> Ok(Style) + "refactor" | "refacto" -> Ok(Refactor) + "perf" -> Ok(Perf) + "test" | "tests" -> Ok(Test) + "build" -> Ok(Build) + "ci" -> Ok(Ci) + "chore" -> Ok(Chore) + "revert" -> Ok(Revert) + _ -> Error(InvalidCommitType) + } +} diff --git a/test/releam_test.gleam b/test/releam_test.gleam new file mode 100644 index 0000000..32bdff1 --- /dev/null +++ b/test/releam_test.gleam @@ -0,0 +1,253 @@ +import gleam/option.{None, Some} +import gleeunit +import gleeunit/should +import releam.{ + Build, Chore, Ci, ConventionalAttributes, Docs, Feat, Fix, + InvalidCommitDefinition, InvalidCommitType, InvalidConventionalFooter, Perf, + Refactor, Revert, Style, Test, +} + +pub fn main() { + gleeunit.main() +} + +// pub fn parse_conventional_attributes_simple_with_breaking_test() { +// "feat!: send an email to the customer when a product is shipped" +// |> releam.parse_conventional_attributes +// |> should.equal(releam.ConventionalAttributes( +// Some(Feat), +// None, +// "send an email to the customer when a product is shipped", +// None, +// None, +// True, +// )) +// } + +// pub fn parse_conventional_attributes_simple_with_scope_test() { +// "chore(deps): bump versions\n" +// |> releam.parse_conventional_attributes +// |> should.equal(ConventionalAttributes( +// Some(Chore), +// Some("deps"), +// "bump versions", +// None, +// None, +// False, +// )) +// } + +// pub fn parse_conventional_attributes_simple_with_scope_and_breaking_test() { +// "chore(deps)!: bump versions\n" +// |> releam.parse_conventional_attributes +// |> should.equal(ConventionalAttributes( +// Some(Chore), +// Some("deps"), +// "bump versions", +// None, +// None, +// True, +// )) +// } + +// pub fn parse_conventional_attributes_with_breaking_and_breaking_footer_test() { +// "feat(api)!: drop support for uids + +// BREAKING CHANGE: drop support for queries using uids" +// |> releam.parse_conventional_attributes +// |> should.equal(ConventionalAttributes( +// Some(Feat), +// Some("api"), +// "drop support for uids", +// None, +// Some([#("BREAKING CHANGE", "drop support for queries using uids")]), +// True, +// )) +// } + +// pub fn parse_conventional_attributes_with_bodies_test() { +// "fix: prevent racing of requests + +// Introduce a request id and a reference to latest request. Dismiss +// incoming responses other than from latest request. + +// Remove timeouts which were used to mitigate the racing issue but are +// obsolete now. +// " +// |> releam.parse_conventional_attributes +// |> should.equal(ConventionalAttributes( +// Some(Fix), +// None, +// "prevent racing of requests", +// Some([ +// "Introduce a request id and a reference to latest request. Dismiss\nincoming responses other than from latest request.", +// "Remove timeouts which were used to mitigate the racing issue but are\nobsolete now.", +// ]), +// None, +// False, +// )) +// } + +// pub fn parse_conventional_attributes_with_body_and_footers_test() { +// "fix: prevent racing of requests + +// Introduce a request id and a reference to latest request. Dismiss +// incoming responses other than from latest request. + +// Remove timeouts which were used to mitigate the racing issue but are +// obsolete now. + +// Reviewed-by: Z +// Refs: #123" +// |> releam.parse_conventional_attributes +// |> should.equal( +// Ok(ConventionalAttributes( +// Fix, +// None, +// "prevent racing of requests", +// [ +// "Introduce a request id and a reference to latest request. Dismiss\nincoming responses other than from latest request.", +// "Remove timeouts which were used to mitigate the racing issue but are\nobsolete now.", +// ], +// [#("Reviewed-by", "Z"), #("Refs", "#123")], +// False, +// )), +// ) +// } + +pub fn parse_conventional_definition_test() { + releam.parse_conventional_definition("feat: lorem ipsum") + |> should.equal( + Ok(ConventionalAttributes( + commit_type: Feat, + scope: None, + message: "lorem ipsum", + body: [], + footer: [], + breaking_change: False, + )), + ) + + releam.parse_conventional_definition("feat(api): lorem ipsum") + |> should.equal( + Ok(ConventionalAttributes( + commit_type: Feat, + scope: Some("api"), + message: "lorem ipsum", + body: [], + footer: [], + breaking_change: False, + )), + ) + + releam.parse_conventional_definition("feat(api)!: lorem ipsum") + |> should.equal( + Ok(ConventionalAttributes( + commit_type: Feat, + scope: Some("api"), + message: "lorem ipsum", + body: [], + footer: [], + breaking_change: True, + )), + ) + + releam.parse_conventional_definition("feat!: lorem ipsum") + |> should.equal( + Ok(ConventionalAttributes( + commit_type: Feat, + scope: None, + message: "lorem ipsum", + body: [], + footer: [], + breaking_change: True, + )), + ) + + releam.parse_conventional_definition("foo: lorem ipsum") + |> should.equal(Error(InvalidCommitType)) + + releam.parse_conventional_definition("lorem ipsum") + |> should.equal(Error(InvalidCommitDefinition)) +} + +pub fn parse_conventional_optional_sections_test() { + [ + "foo bar", "lorem ipsum", + "Reviewed-by: Z + Refs: #123", + ] + |> releam.parse_conventional_optional_sections + |> should.equal( + #(["foo bar", "lorem ipsum"], [#("Reviewed-by", "Z"), #("Refs", "#123")]), + ) +} + +pub fn parse_conventional_optional_sections_with_invalid_footer_test() { + ["foo bar", "lorem ipsum", "Reviewed by: Z"] + |> releam.parse_conventional_optional_sections + |> should.equal(#(["foo bar", "lorem ipsum", "Reviewed by: Z"], [])) +} + +pub fn parse_conventional_footer_test() { + "Reviewed-by: Z + Refs: #123" + |> releam.parse_conventional_footer + |> should.equal(Ok([#("Reviewed-by", "Z"), #("Refs", "#123")])) +} + +pub fn parse_conventional_footer_with_breaking_change_test() { + "Reviewed-by: Z + BREAKING CHANGE: drop json support" + |> releam.parse_conventional_footer + |> should.equal( + Ok([#("Reviewed-by", "Z"), #("BREAKING CHANGE", "drop json support")]), + ) +} + +pub fn parse_conventional_footer_with_invalid_test() { + "Reviewed by: Z" + |> releam.parse_conventional_footer + |> should.equal(Error(InvalidConventionalFooter)) +} + +pub fn parse_conventional_commit_type_test() { + releam.parse_conventional_commit_type("feat") + |> should.equal(Ok(Feat)) + + releam.parse_conventional_commit_type("fix") + |> should.equal(Ok(Fix)) + + releam.parse_conventional_commit_type("docs") + |> should.equal(Ok(Docs)) + + releam.parse_conventional_commit_type("style") + |> should.equal(Ok(Style)) + + releam.parse_conventional_commit_type("refactor") + |> should.equal(Ok(Refactor)) + + releam.parse_conventional_commit_type("refacto") + |> should.equal(Ok(Refactor)) + + releam.parse_conventional_commit_type("perf") + |> should.equal(Ok(Perf)) + + releam.parse_conventional_commit_type("test") + |> should.equal(Ok(Test)) + + releam.parse_conventional_commit_type("tests") + |> should.equal(Ok(Test)) + + releam.parse_conventional_commit_type("build") + |> should.equal(Ok(Build)) + + releam.parse_conventional_commit_type("ci") + |> should.equal(Ok(Ci)) + + releam.parse_conventional_commit_type("chore") + |> should.equal(Ok(Chore)) + + releam.parse_conventional_commit_type("revert") + |> should.equal(Ok(Revert)) +}