Skip to content

Commit

Permalink
Use semver implementation from fsprojects/Paket#3030
Browse files Browse the repository at this point in the history
  • Loading branch information
matthid committed Feb 10, 2018
1 parent ac1472f commit 2e66b36
Showing 1 changed file with 255 additions and 89 deletions.
344 changes: 255 additions & 89 deletions src/app/Fake.Core.SemVer/SemVer.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
namespace Fake.Core

open System
open System.Globalization
open System.Text.RegularExpressions
open Fake.Core

/// Contains active patterns which allow to deal with [Semantic Versioning](http://semver.org/) (SemVer).
module SemVerActivePattern =
Expand Down Expand Up @@ -35,110 +35,197 @@ module SemVerActivePattern =
| ver when ver.Length > 1 && ver.StartsWith("0") -> None
| _ -> Some ValidVersion

module internal InternalHelper =

let ComparePreRelease a b =
let (|Int|_|) str =
match System.Int32.TryParse(str) with
| (true, int) -> Some(int)
| _ -> None

let comp a b =
match (a, b) with
| (Int a, Int b) -> a.CompareTo(b)
| (Int a, _) -> -1
| (_, Int b) -> 1
| _ -> match String.CompareOrdinal(a, b) with
| i when not (i = 0) -> i
| _ -> 0
[<CustomEquality; CustomComparison>]
type PreReleaseSegment =
| AlphaNumeric of string
| Numeric of bigint

let aEmpty = String.IsNullOrEmpty(a)
let bEmpty = String.IsNullOrEmpty(b)
member x.CompareTo(y) =
match x, y with
| AlphaNumeric a, AlphaNumeric b -> compare a b
| Numeric a, Numeric b -> compare a b
| AlphaNumeric _, Numeric _ -> 1
| Numeric _, AlphaNumeric _ -> -1

match (aEmpty, bEmpty) with
| (true, true) -> 0
| (true, false) -> 1
| (false, true) -> -1
| _ -> Seq.compareWith comp (a.Split '.') (b.Split '.')
interface System.IComparable with
member x.CompareTo yobj =
match yobj with
| :? PreReleaseSegment as y -> x.CompareTo(y)
| _ -> invalidArg "yobj" "can't compare to other types of objects."

override x.GetHashCode() = hash x

member x.Equals(y) =
match x, y with
| AlphaNumeric a, AlphaNumeric b -> a = b
| Numeric a, Numeric b -> a = b
| AlphaNumeric _, Numeric _ -> false
| Numeric _, AlphaNumeric _ -> false

open SemVerActivePattern
open InternalHelper
override x.Equals yobj =
match yobj with
| :? PreReleaseSegment as y -> x.Equals(y)
| _ -> false

/// Information about PreRelease packages.
[<CustomEquality; CustomComparison>]
type PreRelease =
{ Origin: string
Name: string }
static member TryParse str =
match str with
| ParseRegex "^(?<name>[0-9A-Za-z\-\.]+)$" [name] ->
Some { Origin = str; Name = name }
| _ ->
None
{ Origin : string
Name : string
Values : PreReleaseSegment list }

static member TryParse (str : string) =
if String.IsNullOrEmpty str then None
else
let getName fromList =
match fromList with
| AlphaNumeric(a)::_ -> a
| _::AlphaNumeric(a)::_ -> a // fallback to 2nd
| _ -> ""

let parse segment =
match bigint.TryParse segment with
| true, number when number >= 0I -> Numeric number
| _ -> AlphaNumeric segment

let notEmpty = StringSplitOptions.RemoveEmptyEntries
let name, values =
match str.Split([|'.'|],notEmpty) with
| [|one|] ->
let list = one.Split([|'-'|],notEmpty) |> Array.map parse |> List.ofArray

// semver1-like embedded / inlined prerelease numbers
let name =
match Regex("^(?<name>[a-zA-Z]+)").Match(one) with
| ex when ex.Success -> ex.Value
| _ -> getName list

name, list

| multiple -> //semver2: dashes are ok, inline numbers not

let list = multiple |> Array.map parse |> List.ofArray
getName list, list

Some { Origin = str; Name = name; Values = values }

override x.Equals(yobj) =
member x.Equals(y) = x.Origin = y.Origin

override x.Equals(yobj) =
match yobj with
| :? PreRelease as y -> x.Origin = y.Origin
| :? PreRelease as y -> x.Equals(y)
| _ -> false

override x.ToString() = x.Origin

override x.GetHashCode() = hash x.Origin

member x.CompareTo(yobj) =
let rec cmp item count xlist ylist =
if item < count then
let res = compare (List.head xlist) (List.head ylist)
if res = 0 then
cmp (item + 1) count (List.tail xlist) (List.tail ylist)
else
res // result given by first difference
else
sign xlist.Length - ylist.Length // https://semver.org/#spec-item-11
let len = min x.Values.Length yobj.Values.Length // compare up to common len
cmp 0 len x.Values yobj.Values

interface System.IComparable with
member x.CompareTo yobj =
match yobj with
| :? PreRelease as y ->
ComparePreRelease x.Name y.Name
| _ -> invalidArg "yobj" "cannot compare values of different types"
| :? PreRelease as y -> x.CompareTo(y)
| _ -> invalidArg "yobj" "PreRelease: cannot compare to values of different types"


/// Contains the version information.
[<CustomEquality; CustomComparison>]
type SemVerInfo =
[<CustomEquality; CustomComparison; StructuredFormatDisplay("{AsString}")>]
type SemVerInfo =
{ /// MAJOR version when you make incompatible API changes.
Major: int
Major : uint32
/// MINOR version when you add functionality in a backwards-compatible manner.
Minor: int
Minor : uint32
/// PATCH version when you make backwards-compatible bug fixes.
Patch: int
Patch : uint32
/// The optional PreRelease version
PreRelease : PreRelease option
/// The optional build no.
Build: string }
override x.ToString() =
sprintf "%d.%d.%d%s%s" x.Major x.Minor x.Patch
(match x.PreRelease with
| Some preRelease -> sprintf "-%s"preRelease.Name
| _ -> "")
(match String.isNotNullOrEmpty x.Build with
| true -> sprintf "+%s" x.Build
| _ -> "")

override x.Equals(yobj) =
Build : bigint
BuildMetaData : string
// The original version text
Original : string option }

member x.Normalize() =
let build =
if x.Build > 0I then ("." + x.Build.ToString("D")) else ""

let pre =
match x.PreRelease with
| Some preRelease -> ("-" + preRelease.Origin)
| None -> ""

sprintf "%d.%d.%d%s%s" x.Major x.Minor x.Patch build pre

member x.NormalizeToShorter() =
let s = x.Normalize()
let s2 = sprintf "%d.%d" x.Major x.Minor
if s = s2 + ".0" then s2 else s

override x.ToString() =
match x.Original with
| Some version -> version.Trim()
| None -> x.Normalize()

member x.AsString
with get() = x.ToString()

member x.Equals(y) =
x.Major = y.Major && x.Minor = y.Minor && x.Patch = y.Patch && x.Build = y.Build && x.PreRelease = y.PreRelease

override x.Equals(yobj) =
match yobj with
| :? SemVerInfo as y ->
(x.Minor,x.Minor,x.Patch,x.PreRelease,x.Build) =
(y.Minor,y.Minor,y.Patch,y.PreRelease,y.Build)
| :? SemVerInfo as y -> x.Equals(y)
| _ -> false

override x.GetHashCode() = hash (x.Minor,x.Minor,x.Patch,x.PreRelease,x.Build)

override x.GetHashCode() = hash (x.Major, x.Minor, x.Patch, x.Build, x.PreRelease)

member x.CompareTo(y) =
let comparison =
match compare x.Major y.Major with
| 0 ->
match compare x.Minor y.Minor with
| 0 ->
match compare x.Patch y.Patch with
| 0 ->
match compare x.Build y.Build with
| 0 ->
match x.PreRelease, y.PreRelease with
| None, None -> 0
| Some _, None -> -1
| None, Some p -> 1
| Some p, Some p2 when p.Origin = "prerelease" && p2.Origin = "prerelease" -> 0
| Some p, _ when p.Origin = "prerelease" -> -1
| _, Some p when p.Origin = "prerelease" -> 1
| Some left, Some right -> compare left right
| c -> c
| c -> c
| c -> c
| c -> c
comparison

interface System.IComparable with
member x.CompareTo yobj =
member x.CompareTo yobj =
match yobj with
| :? SemVerInfo as y ->
if x.Major <> y.Major then compare x.Major y.Major else
if x.Minor <> y.Minor then compare x.Minor y.Minor else
if x.Patch <> y.Patch then compare x.Patch y.Patch else
if x.PreRelease = y.PreRelease && x.Build = y.Build then 0 else
if x.PreRelease.IsNone && x.Build = "" then 1 else
if y.PreRelease.IsNone && y.Build = "" then -1 else
if x.PreRelease <> y.PreRelease then compare x.PreRelease y.PreRelease else
if x.Build <> y.Build then
match Int32.TryParse x.Build, Int32.TryParse y.Build with
| (true,b1),(true,b2) -> compare b1 b2
| _ -> compare x.Build y.Build
else
0
| _ -> invalidArg "yobj" "cannot compare values of different types"
| :? SemVerInfo as y -> x.CompareTo(y)
| _ -> invalidArg "yobj" "SemVerInfo: cannot compare to values of different types"


/// Contains helpers which allow to deal with [Semantic Versioning](http://semver.org/) (SemVer).
/// Parser which allows to deal with [Semantic Versioning](http://semver.org/) (SemVer).
module SemVer =
open System.Numerics

/// Returns true if input appears to be a parsable semver string
let isValidSemVer version =
match version with
Expand All @@ -147,6 +234,38 @@ module SemVer =
| _ ->
false

/// Matches if str is convertible to Int and not less than zero, and returns the value as UInt.
let inline private (|Int|_|) str =
match Int32.TryParse (str, NumberStyles.Integer, null) with
| true, num when num > -1 -> Some num
| _ -> None

/// Matches if str is convertible to big int and not less than zero, and returns the bigint value.
let inline private (|Big|_|) str =
match BigInteger.TryParse (str, NumberStyles.Integer, null) with
| true, big when big > -1I -> Some big
| _ -> None

/// Splits the given version string by possible delimiters but keeps them as parts of resulting list.
let private expand delimiter (text : string) =
let sb = Text.StringBuilder()
let res = seq {
for ch in text do
match List.contains ch delimiter with
| true ->
yield sb.ToString()
sb.Clear() |> ignore
yield ch.ToString()
| false ->
sb.Append(ch) |> ignore
if sb.Length > 0 then
yield sb.ToString()
sb.Clear() |> ignore
}
res |> Seq.toList

let private validContent = Regex(@"(?in)^[a-z0-9-]+(\.[a-z0-9-]+)*")

/// Parses the given version string into a SemVerInfo which can be printed using ToString() or compared
/// according to the rules described in the [SemVer docs](http://semver.org/).
/// ## Sample
Expand All @@ -156,16 +275,63 @@ module SemVer =
/// parse "1.2.3-alpha2" > parse "1.2.3-alpha" // true
/// parse "1.2.3-alpha002" > parse "1.2.3-alpha1" // false
/// parse "1.5.0-beta.2" > parse "1.5.0-rc.1" // false
let parse version =
match version with
| SemVer [major; minor; patch; pre; build] ->
{
Major = if String.isNullOrEmpty major then 1 else Int32.Parse major
Minor = if String.isNullOrEmpty minor then 0 else Int32.Parse minor
Patch = if String.isNullOrEmpty patch then 0 else Int32.Parse patch
PreRelease = PreRelease.TryParse pre
Build = build
}
| _ ->
failwithf "Unable to parse version %s" version
let parse (version : string) =
try
/// sanity check to make sure that all of the integers in the string are positive.
/// because we use raw substrings with dashes this is very complex :(
for s in version.Split([|'.'|]) do
match Int32.TryParse s with
| true, s when s < 0 -> failwith "no negatives!"
| _ -> ignore () // non-numeric parts are valid

if version.Contains("!") then
failwithf "Invalid character found in %s" version
if version.Contains("..") then
failwithf "Empty version part found in %s" version

let plusIndex = version.IndexOf("+")

let versionStr =
match plusIndex with
| n when n < 0 -> version
| n -> version.Substring(0, n)

/// there can only be one piece of build metadata, and it is signified by + sign
/// and then any number of dot-separated alpha-numeric groups.
let buildmeta =
match plusIndex with
| -1 -> ""
| n when n = version.Length - 1 -> ""
| n ->
let content = validContent.Match(version.Substring(n + 1))
if content.Success then content.Value else ""

let fragments = expand [ '.'; '-' ] versionStr
/// matches over list of the version fragments *and* delimiters
let major, minor, patch, revision, suffix =
match fragments with
| (Int M)::"."::(Int m)::"."::(Int p)::"."::(Big b)::tail -> M, m, p, b, tail
| (Int M)::"."::(Int m)::"."::(Int p)::tail -> M, m, p, 0I, tail
| (Int M)::"."::(Int m)::tail -> M, m, 0, 0I, tail
| (Int M)::tail -> M, 0, 0, 0I, tail
| _ -> raise(ArgumentException("SemVer.Parse", "version"))
//this is expected to fail, for now :/
//| [text] -> 0, 0, 0, 0I, [text]
//| [] | _ -> 0, 0, 0, 0I, []

/// recreate the remaining string to parse as prerelease segments
let prerelease =
if suffix.IsEmpty || suffix.Tail.IsEmpty then ""
else String.Concat(suffix.Tail).TrimEnd([|'.'; '-'|])

{ Major = uint32 major
Minor = uint32 minor
Patch = uint32 patch
Build = revision
PreRelease = PreRelease.TryParse prerelease
BuildMetaData = buildmeta
Original = Some version }

with
| exn ->
failwithf "Can't parse \"%s\". %s" version (exn.ToString())

0 comments on commit 2e66b36

Please sign in to comment.