diff --git a/responses.go b/responses.go index ea812d52..19cabfad 100644 --- a/responses.go +++ b/responses.go @@ -16,12 +16,6 @@ type RespCreateFilter struct { FilterID string `json:"filter_id"` } -// RespVersions is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientversions -type RespVersions struct { - Versions []string `json:"versions"` - UnstableFeatures map[string]bool `json:"unstable_features"` -} - // RespJoinRoom is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidjoin type RespJoinRoom struct { RoomID id.RoomID `json:"room_id"` diff --git a/versions.go b/versions.go new file mode 100644 index 00000000..b921bc9f --- /dev/null +++ b/versions.go @@ -0,0 +1,139 @@ +// Copyright (c) 2022 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package mautrix + +import ( + "fmt" + "regexp" + "strconv" +) + +// RespVersions is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientversions +type RespVersions struct { + Versions []SpecVersion `json:"versions"` + UnstableFeatures map[string]bool `json:"unstable_features"` +} + +func (versions *RespVersions) ContainsFunc(match func(found SpecVersion) bool) bool { + for _, found := range versions.Versions { + if match(found) { + return true + } + } + return false +} + +func (versions *RespVersions) Contains(version SpecVersion) bool { + return versions.ContainsFunc(func(found SpecVersion) bool { + return found == version + }) +} + +func (versions *RespVersions) ContainsGreaterOrEqual(version SpecVersion) bool { + return versions.ContainsFunc(func(found SpecVersion) bool { + return found.GreaterThan(version) || found == version + }) +} + +type SpecVersionFormat int + +const ( + SpecVersionFormatUnknown SpecVersionFormat = iota + SpecVersionFormatR + SpecVersionFormatV +) + +var ( + SpecR010 = MustParseSpecVersion("r0.1.0") + SpecR020 = MustParseSpecVersion("r0.2.0") + SpecR030 = MustParseSpecVersion("r0.3.0") + SpecR040 = MustParseSpecVersion("r0.4.0") + SpecR050 = MustParseSpecVersion("r0.5.0") + SpecR060 = MustParseSpecVersion("r0.6.0") + SpecR061 = MustParseSpecVersion("r0.6.1") + SpecV11 = MustParseSpecVersion("v1.1") + SpecV12 = MustParseSpecVersion("v1.2") +) + +func (svf SpecVersionFormat) String() string { + switch svf { + case SpecVersionFormatR: + return "r" + case SpecVersionFormatV: + return "v" + default: + return "" + } +} + +type SpecVersion struct { + Format SpecVersionFormat + Major int + Minor int + Patch int + + Raw string +} + +var legacyVersionRegex = regexp.MustCompile(`^r(\d+)\.(\d+)\.(\d+)$`) +var modernVersionRegex = regexp.MustCompile(`^v(\d+)\.(\d+)$`) + +func MustParseSpecVersion(version string) SpecVersion { + sv, err := ParseSpecVersion(version) + if err != nil { + panic(err) + } + return sv +} + +func ParseSpecVersion(version string) (sv SpecVersion, err error) { + sv.Raw = version + if parts := modernVersionRegex.FindStringSubmatch(version); parts != nil { + sv.Major, _ = strconv.Atoi(parts[1]) + sv.Minor, _ = strconv.Atoi(parts[2]) + sv.Format = SpecVersionFormatV + } else if parts = legacyVersionRegex.FindStringSubmatch(version); parts != nil { + sv.Major, _ = strconv.Atoi(parts[1]) + sv.Minor, _ = strconv.Atoi(parts[2]) + sv.Patch, _ = strconv.Atoi(parts[3]) + sv.Format = SpecVersionFormatR + } else { + err = fmt.Errorf("version '%s' doesn't match either known syntax", version) + } + return +} + +func (sv *SpecVersion) UnmarshalText(version []byte) error { + *sv, _ = ParseSpecVersion(string(version)) + return nil +} + +func (sv *SpecVersion) MarshalText() ([]byte, error) { + return []byte(sv.String()), nil +} + +func (sv *SpecVersion) String() string { + switch sv.Format { + case SpecVersionFormatR: + return fmt.Sprintf("r%d.%d.%d", sv.Major, sv.Minor, sv.Patch) + case SpecVersionFormatV: + return fmt.Sprintf("v%d.%d", sv.Major, sv.Minor) + default: + return sv.Raw + } +} + +func (sv SpecVersion) LessThan(other SpecVersion) bool { + return sv != other && !sv.GreaterThan(other) +} + +func (sv SpecVersion) GreaterThan(other SpecVersion) bool { + return sv.Format > other.Format || + (sv.Format == other.Format && sv.Major > other.Major) || + (sv.Format == other.Format && sv.Major == other.Major && sv.Minor > other.Minor) || + (sv.Format == other.Format && sv.Major == other.Major && sv.Minor == other.Minor && sv.Patch > other.Patch) +} diff --git a/versions_test.go b/versions_test.go new file mode 100644 index 00000000..fbcced9f --- /dev/null +++ b/versions_test.go @@ -0,0 +1,102 @@ +// Copyright (c) 2022 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package mautrix_test + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + + "maunium.net/go/mautrix" +) + +const sampleVersions = `{ + "versions": [ + "r0.0.1", + "r0.1.0", + "r0.2.0", + "r0.3.0", + "r0.4.0", + "r0.5.0", + "r0.6.0", + "r0.6.1", + "v1.1", + "v1.2" + ], + "unstable_features": { + "org.matrix.label_based_filtering": true, + "org.matrix.e2e_cross_signing": true, + "org.matrix.msc2432": true, + "uk.half-shot.msc2666.mutual_rooms": true, + "io.element.e2ee_forced.public": false, + "io.element.e2ee_forced.private": false, + "io.element.e2ee_forced.trusted_private": false, + "org.matrix.msc3026.busy_presence": false, + "org.matrix.msc2285": true, + "org.matrix.msc2716": false, + "org.matrix.msc3030": false, + "org.matrix.msc3440.stable": true, + "fi.mau.msc2815": false + } +}` + +func TestRespVersions_UnmarshalJSON(t *testing.T) { + var resp mautrix.RespVersions + err := json.Unmarshal([]byte(sampleVersions), &resp) + assert.NoError(t, err) + assert.True(t, resp.ContainsGreaterOrEqual(mautrix.SpecV11)) + assert.True(t, resp.Contains(mautrix.SpecV12)) + assert.True(t, resp.Contains(mautrix.SpecR061)) + assert.True(t, resp.ContainsGreaterOrEqual(mautrix.MustParseSpecVersion("r0.0.0"))) + assert.True(t, !resp.ContainsGreaterOrEqual(mautrix.MustParseSpecVersion("v123.456"))) +} + +func TestParseSpecVersion(t *testing.T) { + assert.Equal(t, + mautrix.SpecVersion{mautrix.SpecVersionFormatR, 0, 1, 0, "r0.1.0"}, + mautrix.MustParseSpecVersion("r0.1.0")) + assert.Equal(t, + mautrix.SpecVersion{mautrix.SpecVersionFormatV, 1, 1, 0, "v1.1"}, + mautrix.MustParseSpecVersion("v1.1")) + assert.Equal(t, + mautrix.SpecVersion{mautrix.SpecVersionFormatV, 123, 456, 0, "v123.456"}, + mautrix.MustParseSpecVersion("v123.456")) + + invalidVer, err := mautrix.ParseSpecVersion("not a version") + assert.Error(t, err) + assert.Equal(t, mautrix.SpecVersion{Raw: "not a version"}, invalidVer) + + // v syntax doesn't allow patch versions + invalidVer, err = mautrix.ParseSpecVersion("v1.2.3") + assert.Error(t, err) + assert.Equal(t, mautrix.SpecVersion{Raw: "v1.2.3"}, invalidVer) + + invalidVer, err = mautrix.ParseSpecVersion("r0.6") + assert.Error(t, err) + assert.Equal(t, mautrix.SpecVersion{Raw: "r0.6"}, invalidVer) +} + +func TestSpecVersion_String(t *testing.T) { + assert.Equal(t, "r0.1.0", (&mautrix.SpecVersion{mautrix.SpecVersionFormatR, 0, 1, 0, ""}).String()) + assert.Equal(t, "v1.2", (&mautrix.SpecVersion{mautrix.SpecVersionFormatV, 1, 2, 0, ""}).String()) + assert.Equal(t, "v567.890", (&mautrix.SpecVersion{mautrix.SpecVersionFormatV, 567, 890, 0, ""}).String()) + assert.Equal(t, "invalid version", (&mautrix.SpecVersion{Raw: "invalid version"}).String()) +} + +func TestSpecVersion_GreaterThan(t *testing.T) { + assert.True(t, mautrix.MustParseSpecVersion("r0.1.0").GreaterThan(mautrix.MustParseSpecVersion("r0.0.0"))) + assert.True(t, mautrix.MustParseSpecVersion("r0.6.0").GreaterThan(mautrix.MustParseSpecVersion("r0.1.0"))) + assert.True(t, mautrix.MustParseSpecVersion("r0.6.1").GreaterThan(mautrix.MustParseSpecVersion("r0.1.0"))) + assert.True(t, mautrix.MustParseSpecVersion("v1.1").GreaterThan(mautrix.MustParseSpecVersion("r0.6.1"))) + assert.True(t, mautrix.MustParseSpecVersion("v11.11").GreaterThan(mautrix.MustParseSpecVersion("v1.23"))) + assert.True(t, mautrix.MustParseSpecVersion("v1.123").GreaterThan(mautrix.MustParseSpecVersion("v1.1"))) + assert.True(t, !mautrix.MustParseSpecVersion("v1.23").GreaterThan(mautrix.MustParseSpecVersion("v2.31"))) + assert.True(t, !mautrix.MustParseSpecVersion("r0.6.0").GreaterThan(mautrix.MustParseSpecVersion("r0.6.1"))) + assert.True(t, !mautrix.MustParseSpecVersion("r0.6.0").GreaterThan(mautrix.MustParseSpecVersion("r0.6.0"))) + assert.True(t, !mautrix.MustParseSpecVersion("r0.6.0").LessThan(mautrix.MustParseSpecVersion("r0.6.0"))) +}