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)) }