# `semver_dialects`

`semver_dialects` is a gem to parse and compare versions.
It supports multiple syntaxes, including variants of Semver.

`semver_dialects` can also match a version against a simple interval like `[1.0, 2.0]`
or against a set of intervals like `[1.0, 2.0], [3.0, 3.1[` (in Maven syntax).
Intervals are also known as ranges.
Interval sets are disjunctions of intervals/ranges.

You can find a detailed description of the theoretical foundation we used for `semver_dialects` in this [blog post](https://about.gitlab.com/blog/2021/09/28/generic-semantic-version-processing/)

## Supported syntaxes

| Package type | Specs for versions | Specs for ranges | Example |
| ------------ | ------------------ | ---------------- | ------- |
| `apk` | [Versioning](https://wiki.alpinelinux.org/wiki/APKBUILD_Reference#pkgver) | - |  |
| `cargo` | [Semver v2.0.0](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html) | [Version requirement syntax](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#version-requirement-syntax) | `>=1.2.3, <1.3.0 \|\| >=1.0.0, <2.0.0` |
| `conan` | [Versions](https://docs.conan.io/2/tutorial/versioning/versions.html) | [Version ranges](https://docs.conan.io/2/tutorial/versioning/version_ranges.html) | `>=1.0 <2.0 \|\| >3.0` |
| `deb` | [Versions](https://www.debian.org/doc/debian-policy/ch-controlfields.html#version) | [Version comparisons](https://github.com/captn3m0/ruby-deb-version/blob/a390b7d23212b50e5acd28b544e37b5eac460c19/test.rb#L48)| `1:0.5`
| `gem` | - | - | `>=2.0.0 <=2.12.4\|\|>=3.0.0 <=4.0.0.beta7` |
| `swift` | [Semver v2.0.0](https://semver.org/) | - | `>=2.0.0 <=2.12.4\|\|>=3.0.0 <=4.0.0.beta7` |
| `pub` | [Semver v2.0.0](https://dart.dev/tools/pub/versioning) | - | `>=2.0.0 <=2.12.4\|\|>=3.0.0 <=4.0.0.beta7` |
| `go` | [Go versions](https://go.dev/ref/mod#versions) | [Version queries](https://go.dev/ref/mod#version-queries) | `>=1.11.0 <1.11.9\|\|>=1.12.0 <1.12.7\|\|>=1.13.0 <1.13.5\|\|=1.14.0` |
| `maven` | [Version order specification](https://maven.apache.org/pom.html#version-order-specification) | [Dependency Version Requirement Specification](https://maven.apache.org/pom.html#Dependency_Version_Requirement_Specification) | `[1.0-alpha0,1.0.3.RELEASE],[1.1-alpha0,1.1.2.RELEASE)` |
| `npm` | [Semver v2.0.0](https://semver.org/) | [Ranges](https://github.com/npm/node-semver#ranges) | `<1.6.5 \|\| <2.1.7 >=2.0.0-beta0` |
| `nuget` | [Package versioning](https://learn.microsoft.com/en-us/nuget/concepts/package-versioning) | [Version ranges](https://learn.microsoft.com/en-us/nuget/concepts/package-versioning?tabs=semver20sort#version-ranges) | `[1.0,2.0]` |
| `packagist` | -  | [Writing version constraints](https://getcomposer.org/doc/articles/versions.md#writing-version-constraints) | `<2.2.8\|\|>=2.3.0-alpha0,<2.3.3` |
| `pypi` | [Version specifiers](https://www.python.org/dev/peps/pep-0440/#version-specifiers) | [Version specifiers](https://packaging.python.org/en/latest/specifications/version-specifiers/#id5) | `>=3.0.0,<=3.0.17\|\|>=3.2.0,<=3.2.17\|\|>=3.4.0,<=3.4.17` |
| `rpm` | [Versioning](https://github.com/rpm-software-management/rpm/blob/4d1b7401415003720ea9bef7bda248f7de4fa025/docs/manual/dependencies.md#versioning) | - |  |

The package type can be passed to the following methods:

- `SemverDialects.parse_version`
- `SemverDialects.version_satisfies?`
- `SemverDialects::IntervalParser.parse`
- `SemverDialects::IntervalSetParser.parse`

Additionally, the `apk`, `deb` and `rpm` types can be passed to `version_satisfies?`
to match versions against a less-than constraint. Other types of constraints are not
supported.

## Installation

`semver_dialects` is available on [rubygems.org](https://rubygems.org/gems/semver_dialects).
You can install it by adding the following line to your application's Gemfile:

```ruby
gem 'semver_dialects'
```

And then execute:

    $ bundle

Or install it yourself as:

    $ gem install semver_dialects

## Usage

### Simple parsing and matching

`parse_version` parses returns a version object that is [`Comparable`](https://docs.ruby-lang.org/en/master/Comparable.html).
It calls the parser corresponding to the given package type.

``` ruby
rel = SemverDialects.parse_version("npm", "1.0.0")
pre = SemverDialects.parse_version("npm", "1.0.0-pre.1")
puts rel > pre
# true

final = SemverDialects.parse_version("maven", "1.0ga")
post = SemverDialects.parse_version("maven", "1.0-20240426")
puts post > final
# true
```

Parsers raise `SemverDialects::InvalidVersionError` when the version isn't valid.

``` ruby
begin
  SemverDialects.parse_version("npm", "x.2")
rescue SemverDialects::InvalidVersionError => e
  puts e.message
end
# scan did not consume 'x.2'
```

`version_satisfies?` tells whether a version satisfies a constraint.
It parses the given version and the interval set in the syntax corresponding
to the package type, and returns `true` if the version is in that interval set.

``` ruby
puts SemverDialects.version_satisfies?("npm", "1.0.0-pre.1", "<1.0.0")
# true

puts SemverDialects.version_satisfies?("maven", "1.0-20240426", "[,1.0]")
# false

puts SemverDialects.version_satisfies?("maven", "1.0-20240426", "[1.0,]")
# true
```

### Constraint Syntax - Everything is a Range: Linear Interval Arithmetic

For representing version constraints, `semver_dialects` uses
[linear intervals](https://www.math.kit.edu/ianm2/~kulisch/media/arjpkx.pdf).

``` ruby
puts SemverDialects::IntervalParser.parse('gem', '>=1, <=2')
# [1,2]
puts SemverDialects::IntervalParser.parse('gem', '>1, <=2')
# (1,2]
puts SemverDialects::IntervalParser.parse('gem', '>=1, <2')
# [1,2)
puts SemverDialects::IntervalParser.parse('gem', '>1, <2')
# (1,2)
puts SemverDialects::IntervalParser.parse('gem', '<=2')
# (-inf,2]
puts SemverDialects::IntervalParser.parse('gem', '>=1')
# [1,+inf)
```

For solving constraints, `semver_dialects` leverages
[interval arithmetic](https://www.math.kit.edu/ianm2/~kulisch/media/arjpkx.pdf).

#### Intersection

Interval intersection can be used to compute the overlap between of two intervals.

``` ruby
puts SemverDialects::IntervalParser.parse("gem", ">=2, <=5").intersect(SemverDialects::IntervalParser.parse("gem", ">=3, <=10"))
# [3,5]
puts SemverDialects::IntervalParser.parse("gem", ">=2, <=5").intersect(SemverDialects::IntervalParser.parse("gem", ">=7, <=10"))
# empty
```

#### Version Matching

The examples below illustrate how you can verify whether a version falls into a
specific range.

``` ruby
r1 = SemverDialects::IntervalSet.new
r1.add(SemverDialects::IntervalParser.parse("gem", ">=2.1.2, <=5.1.2")); r1.add(SemverDialects::IntervalParser.parse("gem", ">3.1, <10"))

puts "[0,2.1) in #{r1}? #{r1.overlaps_with?(SemverDialects::IntervalParser.parse("gem", ">=0, <2.1"))}"
# [0,2.1) in [2.1.2,5.1.2],(3.1,10)? false
```

#### Interval set translation

`IntervalSetParser.translate` extracts intervals from a string representing an interval set.
It returns an array of string. Each string represents an interval.

``` ruby
puts SemverDialects::IntervalSetParser.translate("packagist", "<2.5.9||>=2.6.0,<2.6.11").to_s
# ["<2.5.9", ">=2.6.0 <2.6.11"]
```

### Available commands

Commands can be used with the `exe/semver_dialects` file.

#### `check_version`

Checks whether a version matches some constraint.

```
# semver_dialects check_version TYPE VERSION CONSTRAINT

$ semver_dialects check_version go 1.2 \<1.2
1.2 does not match <1.2

$ semver_dialects check_version go 1.2 \<1.3
1.2 matches <1.3 for go
```

#### `sort`

Sorts a given list of versions.

```
$ semver_dialects sort go 1.2 1.3 1.2.3
Sorted versions: 1.2, 1.2.3, 1.3

$ semver_dialects sort go 1.2 1.3 invalid 1.2.3
Warning: Invalid version 'invalid' - scan did not consume 'invalid'
Sorted versions: 1.2, 1.2.3, 1.3

$ semver_dialects sort --json go 1.2 1.3 invalid 1.2.3
{"versions":["1.2","1.2.3","1.3"],"invalid":["invalid"]}
```

## Releasing new version of the gem

After an MR has merged into the main branch, the new gem can be pushed to rubygems via the manual `publish gem` job.

The gem push command uses the `GEM_HOST_API_KEY` ci/cd variable generated for the `gitlab-composition-analysis` account in rubygems (see password db).

## Credits

We would like to thank the following authors very much for their valuable
contributions.

| Author         | MRs/Issues            |
| -------------- | --------------------- |
| @dkniffin      | !2                    |

## Copyright

Copyright (c) 2020 GitLab Inc. See [MIT License](LICENSE.txt) for further details.
