Skip to content

Commit

Permalink
feat: add topic
Browse files Browse the repository at this point in the history
  • Loading branch information
thinkgos committed Nov 29, 2024
1 parent 68579a9 commit 52f92b1
Show file tree
Hide file tree
Showing 5 changed files with 1,138 additions and 1 deletion.
97 changes: 97 additions & 0 deletions topic/topic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Package topic implements common methods to handle MQTT topics.
package topic

import (
"errors"
"strings"
)

// ErrZeroLength is returned by Parse if a topics has a zero length.
var ErrZeroLength = errors.New("zero length topic")

// ErrWildcards is returned by Parse if a topic contains invalid wildcards.
var ErrWildcards = errors.New("invalid use of wildcards")

// Parse removes duplicate and trailing slashes from the supplied
// string and returns the normalized topic.
func Parse(topic string, allowWildcards bool) (string, error) {
// check for zero length
if topic == "" {
return "", ErrZeroLength
}

// normalize topic
if hasAdjacentSlashes(topic) {
topic = collapseSlashes(topic)
}

// remove trailing slashes
topic = strings.TrimRightFunc(topic, trimSlash)

// check again for zero length
if topic == "" {
return "", ErrZeroLength
}

// get first segment
remainder := topic
segment := topicSegment(topic, "/")

// check all segments
for segment != topicEnd {
// check use of wildcards
if (strings.Contains(segment, "+") || strings.Contains(segment, "#")) && len(segment) > 1 {
return "", ErrWildcards
}

// check if wildcards are allowed
if !allowWildcards && (segment == "#" || segment == "+") {
return "", ErrWildcards
}

// check if hash is the last character
if segment == "#" && topicShorten(remainder, "/") != topicEnd {
return "", ErrWildcards
}

// get next segment
remainder = topicShorten(remainder, "/")
segment = topicSegment(remainder, "/")
}

return topic, nil
}

// ContainsWildcards tests if the supplied topic contains wildcards. The topic
// is expected to be tested and normalized using Parse beforehand.
func ContainsWildcards(topic string) bool {
return strings.Contains(topic, "+") || strings.Contains(topic, "#")
}

func hasAdjacentSlashes(str string) bool {
var last rune

for _, r := range str {
if r == '/' && last == '/' {
return true
}
last = r
}
return false
}

func collapseSlashes(str string) string {
var b strings.Builder
var last rune

for _, r := range str {
if r == '/' && last == '/' {
continue
}
b.WriteRune(r)
last = r
}
return b.String()
}

func trimSlash(r rune) bool { return r == '/' }
109 changes: 109 additions & 0 deletions topic/topic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package topic

import (
"testing"

"github.com/stretchr/testify/require"
)

func Test_Parse(t *testing.T) {
tests := map[string]string{
"topic/hello": "topic/hello",
"topic//hello": "topic/hello",
"topic///hello": "topic/hello",
"/topic": "/topic",
"//topic": "/topic",
"///topic": "/topic",
"topic/": "topic",
"topic//": "topic",
"topic///": "topic",
"topic///cool//hello": "topic/cool/hello",
"topic//cool///hello": "topic/cool/hello",
}

for str, result := range tests {
str, err := Parse(str, true)
require.Equal(t, result, str)
require.NoError(t, err, str)
}
}

func Test_ParseZeroLengthError(t *testing.T) {
_, err := Parse("", true)
require.Equal(t, ErrZeroLength, err)

_, err = Parse("/", true)
require.Equal(t, ErrZeroLength, err)

_, err = Parse("//", true)
require.Equal(t, ErrZeroLength, err)
}

func Test_ParseDisallowWildcards(t *testing.T) {
tests := map[string]bool{
"topic": true,
"topic/hello": true,
"topic/cool/hello": true,
"+": false,
"#": false,
"topic/+": false,
"topic/#": false,
}

for str, result := range tests {
_, err := Parse(str, false)

if result {
require.NoError(t, err, str)
} else {
require.Error(t, err, str)
}
}
}

func Test_ParseAllowWildcards(t *testing.T) {
tests := map[string]bool{
"topic": true,
"topic/hello": true,
"topic/cool/hello": true,
"+": true,
"#": true,
"topic/+": true,
"topic/#": true,
"topic/+/hello": true,
"topic/cool/+": true,
"topic/cool/#": true,
"+/cool/#": true,
"+/+/#": true,
"": false,
"++": false,
"##": false,
"#/+": false,
"#/#": false,
}

for str, result := range tests {
_, err := Parse(str, true)

if result {
require.NoError(t, err, str)
} else {
require.Error(t, err, str)
}
}
}

func Test_ContainsWildcards(t *testing.T) {
require.True(t, ContainsWildcards("topic/+"))
require.True(t, ContainsWildcards("topic/#"))
require.False(t, ContainsWildcards("topic/hello"))
}

func Benchmark_Parse(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := Parse("foo", true)
if err != nil {
panic(err)
}
}
}
Loading

0 comments on commit 52f92b1

Please sign in to comment.