242 lines
6.6 KiB
Go
242 lines
6.6 KiB
Go
package version
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Version defines a structure describing the version of a program or module.
|
|
type Version struct {
|
|
Major uint `json:"major"`
|
|
Minor uint `json:"minor"`
|
|
Revision uint `json:"revision"`
|
|
Build uint `json:"build"`
|
|
}
|
|
|
|
// String converts a Version into its generic string representation
|
|
func (version Version) String() string {
|
|
return version.SpecificString(0)
|
|
}
|
|
|
|
// SpecificString converts a Version into its string representation with either the specified number of components ([1,4]) or its generic string representation
|
|
func (version Version) SpecificString(numberOfComponents int) string {
|
|
switch numberOfComponents {
|
|
case 4:
|
|
return fmt.Sprintf("%d.%d.%d.%d", version.Major, version.Minor, version.Revision, version.Build)
|
|
case 3:
|
|
return fmt.Sprintf("%d.%d.%d", version.Major, version.Minor, version.Revision)
|
|
case 2:
|
|
return fmt.Sprintf("%d.%d", version.Major, version.Minor)
|
|
case 1:
|
|
return fmt.Sprintf("%d", version.Major)
|
|
default:
|
|
if 0 < version.Build {
|
|
return fmt.Sprintf("%d.%d.%d.%d", version.Major, version.Minor, version.Revision, version.Build)
|
|
} else if 0 < version.Revision {
|
|
return fmt.Sprintf("%d.%d.%d", version.Major, version.Minor, version.Revision)
|
|
} else if 0 < version.Minor {
|
|
return fmt.Sprintf("%d.%d", version.Major, version.Minor)
|
|
}
|
|
return fmt.Sprintf("%d", version.Major)
|
|
}
|
|
}
|
|
|
|
// Equal verifies, whether two Versions are the same
|
|
func (version Version) Equal(comparand Version) bool {
|
|
return version.Major == comparand.Major && version.Minor == comparand.Minor && version.Revision == comparand.Revision && version.Build == comparand.Build
|
|
}
|
|
|
|
// Smaller verifies, whether the Version is smaller than the comparand
|
|
func (version Version) Smaller(comparand Version) bool {
|
|
if version.Major < comparand.Major {
|
|
return true
|
|
}
|
|
if version.Major == comparand.Major {
|
|
if version.Minor < comparand.Minor {
|
|
return true
|
|
}
|
|
if version.Minor == comparand.Minor {
|
|
if version.Revision < comparand.Revision {
|
|
return true
|
|
}
|
|
if version.Revision == comparand.Revision {
|
|
if version.Build < comparand.Build {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Larger verifies, whether the Version is larger than the comparand
|
|
func (version Version) Larger(comparand Version) bool {
|
|
if version.Equal(comparand) {
|
|
return false
|
|
}
|
|
return !version.Smaller(comparand)
|
|
}
|
|
|
|
func parseError(part, value string) error {
|
|
return fmt.Errorf("the '%s' component of the version could not be parsed ('%s')", part, value)
|
|
}
|
|
|
|
// ParseVersion tries to read all available parts of a version string
|
|
func ParseVersion(text string) (Version, error) {
|
|
result := Version{}
|
|
err := result.Parse(text)
|
|
return result, err
|
|
}
|
|
|
|
// Parse tries to read all available parts of a version string to the internal variables
|
|
func (version *Version) Parse(text string) error {
|
|
if nil == version {
|
|
return fmt.Errorf("cannot parse to nil")
|
|
}
|
|
parts := strings.Split(text, ".")
|
|
if 0 < len(parts) {
|
|
var err error
|
|
major, err := strconv.Atoi(strings.Trim(parts[0], " \t\r\n"))
|
|
if nil != err || major < 0 {
|
|
return parseError("Major", parts[0])
|
|
}
|
|
version.Major = uint(major)
|
|
if 1 < len(parts) {
|
|
minor, err := strconv.Atoi(strings.Trim(parts[1], " \t\r\n"))
|
|
if nil != err || minor < 0 {
|
|
return parseError("Minor", parts[1])
|
|
}
|
|
version.Minor = uint(minor)
|
|
if 2 < len(parts) {
|
|
revision, err := strconv.Atoi(strings.Trim(parts[2], " \t\r\n"))
|
|
if nil != err || revision < 0 {
|
|
return parseError("Revision", parts[2])
|
|
}
|
|
version.Revision = uint(revision)
|
|
if 3 < len(parts) {
|
|
build, err := strconv.Atoi(strings.Trim(parts[3], " \t\r\n"))
|
|
if nil != err || build < 0 {
|
|
return parseError("Build", parts[3])
|
|
}
|
|
version.Build = uint(build)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MarshalXML satisfies the xml.Marshaler interface
|
|
func (version Version) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
|
|
if err = e.EncodeToken(start); err == nil {
|
|
stringValue := version.String()
|
|
if err = e.EncodeToken(xml.CharData([]byte(stringValue))); err == nil {
|
|
if err = e.EncodeToken(xml.EndElement{
|
|
Name: start.Name,
|
|
}); err == nil {
|
|
return e.Flush()
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// MarshalJSON satisfies the json.Marshaler interface
|
|
func (version Version) MarshalJSON() ([]byte, error) {
|
|
return []byte(fmt.Sprintf("\"%s\"", version.String())), nil
|
|
}
|
|
|
|
// UnmarshalXML satisfies the xml.Unmarshaler interface
|
|
func (version *Version) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
var (
|
|
token xml.Token
|
|
err error
|
|
)
|
|
|
|
if nil == version {
|
|
return fmt.Errorf("cannot unmarshal to nil")
|
|
}
|
|
|
|
versionAttributeFound := false
|
|
if nil != start.Attr {
|
|
for _, attribute := range start.Attr {
|
|
switch attribute.Name.Local {
|
|
case "Major":
|
|
major, err := strconv.Atoi(strings.Trim(attribute.Value, " \t\r\n"))
|
|
if nil != err || major < 0 {
|
|
return parseError("Major", attribute.Value)
|
|
}
|
|
version.Major = uint(major)
|
|
versionAttributeFound = true
|
|
case "Minor":
|
|
minor, err := strconv.Atoi(strings.Trim(attribute.Value, " \t\r\n"))
|
|
if nil != err || minor < 0 {
|
|
return parseError("Minor", attribute.Value)
|
|
}
|
|
version.Minor = uint(minor)
|
|
versionAttributeFound = true
|
|
case "Revision":
|
|
revision, err := strconv.Atoi(strings.Trim(attribute.Value, " \t\r\n"))
|
|
if nil != err || revision < 0 {
|
|
return parseError("Revision", attribute.Value)
|
|
}
|
|
version.Revision = uint(revision)
|
|
versionAttributeFound = true
|
|
case "Build":
|
|
build, err := strconv.Atoi(strings.Trim(attribute.Value, " \t\r\n"))
|
|
if nil != err || build < 0 {
|
|
return parseError("Build", attribute.Value)
|
|
}
|
|
version.Build = uint(build)
|
|
versionAttributeFound = true
|
|
}
|
|
}
|
|
}
|
|
|
|
Loop:
|
|
for {
|
|
if token, err = d.Token(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if token == nil {
|
|
break
|
|
}
|
|
|
|
switch se := token.(type) {
|
|
case xml.StartElement:
|
|
return errors.New("expected version string (eg. '1.0.0.0')")
|
|
case xml.CharData:
|
|
if !versionAttributeFound {
|
|
stringData := strings.Trim(string(se), " \t\r\n")
|
|
if 0 < len(stringData) {
|
|
version.Parse(stringData)
|
|
}
|
|
}
|
|
case xml.EndElement:
|
|
break Loop
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func unquoteBytes(s []byte) (t []byte, ok bool) {
|
|
if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' {
|
|
return
|
|
}
|
|
return s[1 : len(s)-1], true
|
|
}
|
|
|
|
// UnmarshalJSON satisfies the json.Unmarshaler interface
|
|
func (version *Version) UnmarshalJSON(data []byte) error {
|
|
cleanData, ok := unquoteBytes(data)
|
|
if ok {
|
|
return version.Parse(string(cleanData))
|
|
}
|
|
return fmt.Errorf("'%s' cannot be unmarshalled from JSON", string(data))
|
|
}
|