Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add regexp matcher #20

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 0 additions & 61 deletions dispatcher.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package osc

import (
"strings"
"time"

"github.com/pkg/errors"
)

Expand All @@ -30,61 +27,3 @@ type Dispatcher interface {
Dispatch(bundle Bundle, exactMatch bool) error
Invoke(msg Message, exactMatch bool) error
}

// PatternMatching is a dispatcher that implements OSC 1.0 pattern matching.
// See http://opensoundcontrol.org/spec-1_0 "OSC Message Dispatching and Pattern Matching"
type PatternMatching map[string]MessageHandler

// Dispatch invokes an OSC bundle's messages.
func (h PatternMatching) Dispatch(b Bundle, exactMatch bool) error {
var (
now = time.Now()
tt = b.Timetag.Time()
)
if tt.Before(now) {
return h.immediately(b, exactMatch)
}
<-time.After(tt.Sub(now))
return h.immediately(b, exactMatch)
}

// immediately invokes an OSC bundle immediately.
func (h PatternMatching) immediately(b Bundle, exactMatch bool) error {
for _, p := range b.Packets {
errs := []string{}
if err := h.invoke(p, exactMatch); err != nil {
errs = append(errs, err.Error())
}
if len(errs) > 0 {
return errors.New(strings.Join(errs, " and "))
}
return nil
}
return nil
}

// invoke invokes an OSC packet, which could be a message or a bundle of messages.
func (h PatternMatching) invoke(p Packet, exactMatch bool) error {
switch x := p.(type) {
case Message:
return h.Invoke(x, exactMatch)
case Bundle:
return h.immediately(x, exactMatch)
default:
return errors.Errorf("unsupported type for dispatcher: %T", p)
}
}

// Invoke invokes an OSC message.
func (h PatternMatching) Invoke(msg Message, exactMatch bool) error {
for address, handler := range h {
matched, err := msg.Match(address, exactMatch)
if err != nil {
return err
}
if matched {
return handler.Handle(msg)
}
}
return nil
}
10 changes: 5 additions & 5 deletions message.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,19 @@ func (msg Message) Equal(other Packet) bool {
}

// Match returns true if the address of the OSC Message matches the given address.
func (msg Message) Match(address string, exactMatch bool) (bool, error) {
func (msg Message) Match(addressPattern string, exactMatch bool) (bool, error) {
if exactMatch {
return address == msg.Address, nil
return addressPattern == msg.Address, nil
}
// Verify same number of parts.
if !VerifyParts(address, msg.Address) {
if !VerifyParts(addressPattern, msg.Address) {
return false, nil
}
exp, err := GetRegex(msg.Address)
exp, err := GetRegex(addressPattern)
if err != nil {
return false, err
}
return exp.MatchString(address), nil
return exp.MatchString(msg.Address), nil
}

// Typetags returns a padded byte slice of the message's type tags.
Expand Down
8 changes: 4 additions & 4 deletions message_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ func TestMatch(t *testing.T) {
{"/path/to/method*", "/path/to/method"},
{"/path/to/m[aei]thod", "/path/to/method"},
} {
msg := Message{Address: pair[0]}
match, err := msg.Match(pair[1], false)
msg := Message{Address: pair[1]}
match, err := msg.Match(pair[0], false)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -113,8 +113,8 @@ func TestMatch(t *testing.T) {
}

msg := Message{Address: `/[`}
if _, err := msg.Match(`/a`, false); err == nil {
t.Fatalf("expected error, got nil")
if _, err := msg.Match(`/a`, false); err != nil {
t.Fatalf("expected nil, got error")
}
}

Expand Down
67 changes: 67 additions & 0 deletions pattern_matching.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package osc

import (
"fmt"
"strings"
"time"

"github.com/pkg/errors"
)

// PatternMatching is a dispatcher that implements OSC 1.0 pattern matching.
// See http://opensoundcontrol.org/spec-1_0 "OSC Message Dispatching and Pattern Matching"
type PatternMatching map[string]MessageHandler

// Dispatch invokes an OSC bundle's messages.
func (h PatternMatching) Dispatch(b Bundle, exactMatch bool) error {
var (
now = time.Now()
tt = b.Timetag.Time()
)
if tt.Before(now) {
return h.immediately(b, exactMatch)
}
<-time.After(tt.Sub(now))
return h.immediately(b, exactMatch)
}

// immediately invokes an OSC bundle immediately.
func (h PatternMatching) immediately(b Bundle, exactMatch bool) error {
for _, p := range b.Packets {
errs := []any{}
if err := h.invoke(p, exactMatch); err != nil {
errs = append(errs, err)
}
if len(errs) > 0 {
return fmt.Errorf("failed to invoke osc bundle "+strings.Repeat(": %w", len(errs)), errs...)
}
return nil
}
return nil
}

// invoke invokes an OSC packet, which could be a message or a bundle of messages.
func (h PatternMatching) invoke(p Packet, exactMatch bool) error {
switch x := p.(type) {
case Message:
return h.Invoke(x, exactMatch)
case Bundle:
return h.immediately(x, exactMatch)
default:
return errors.Errorf("unsupported type for dispatcher: %T", p)
}
}

// Invoke invokes an OSC message.
func (h PatternMatching) Invoke(msg Message, exactMatch bool) error {
for address, handler := range h {
matched, err := msg.Match(address, exactMatch)
if err != nil {
return err
}
if matched {
return handler.Handle(msg)
}
}
return nil
}
14 changes: 7 additions & 7 deletions dispatcher_test.go → pattern_matching_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
)

// Test a successful method invocation.
func TestDispatcherDispatchOK(t *testing.T) {
func TestPatternMatchingDispatcherDispatchOK(t *testing.T) {
c := make(chan struct{})
d := PatternMatching{
"/bar": Method(func(msg Message) error {
Expand All @@ -30,7 +30,7 @@ func TestDispatcherDispatchOK(t *testing.T) {
}

// Test a method that returns an error.
func TestDispatcherDispatchError(t *testing.T) {
func TestPatternMatchingDispatcherDispatchError(t *testing.T) {
d := PatternMatching{
"/foo": Method(func(msg Message) error {
return errors.New("oops")
Expand All @@ -48,7 +48,7 @@ func TestDispatcherDispatchError(t *testing.T) {
}
}

func TestDispatcherDispatchNestedBundle(t *testing.T) {
func TestPatternMatchingDispatcherDispatchNestedBundle(t *testing.T) {
c := make(chan struct{})
d := PatternMatching{
"/foo": Method(func(msg Message) error {
Expand All @@ -74,7 +74,7 @@ func TestDispatcherDispatchNestedBundle(t *testing.T) {
<-c
}

func TestDispatcherMiss(t *testing.T) {
func TestPatternMatchingDispatcherMiss(t *testing.T) {
d := PatternMatching{
"/foo": Method(func(msg Message) error {
return nil
Expand All @@ -88,7 +88,7 @@ func TestDispatcherMiss(t *testing.T) {
}
}

func TestDispatcherInvoke(t *testing.T) {
func TestPatternMatchingDispatcherInvoke(t *testing.T) {
d := PatternMatching{
"/foo": Method(func(msg Message) error {
return errors.New("foo error")
Expand All @@ -102,8 +102,8 @@ func TestDispatcherInvoke(t *testing.T) {
t.Fatal("expected error, got nil")
}
badMsg := Message{Address: "/["}
if err := d.Invoke(badMsg, false); err == nil {
t.Fatal("expected error, got nil")
if err := d.Invoke(badMsg, false); err != nil {
t.Fatal("expected nil, got error")
}
if err := d.Invoke(Message{Address: "/bar"}, false); err != nil {
t.Fatal(err)
Expand Down
70 changes: 70 additions & 0 deletions regexp_matching.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package osc

import (
"fmt"
"regexp"
"strings"
"time"

"github.com/pkg/errors"
)

// RegexpMatching is a dispatcher that simply uses a regexp to match a message.
// If you are looking for the official OSC Pattern Matching dispatcher, see [PatternMatching].

type RegexpMatching map[string]MessageHandler

// Dispatch invokes an OSC bundle's messages.
func (h RegexpMatching) Dispatch(b Bundle, exactMatch bool) error {
var (
now = time.Now()
tt = b.Timetag.Time()
)
if tt.Before(now) {
return h.immediately(b, exactMatch)
}
<-time.After(tt.Sub(now))
return h.immediately(b, exactMatch)
}

// immediately invokes an OSC bundle immediately.
func (h RegexpMatching) immediately(b Bundle, exactMatch bool) error {
for _, p := range b.Packets {
errs := []any{}
if err := h.invoke(p, exactMatch); err != nil {
errs = append(errs, err)
}
if len(errs) > 0 {
return fmt.Errorf("failed to invoke osc bundle "+strings.Repeat(": %w", len(errs)), errs...)
}
}
return nil
}

// invoke invokes an OSC packet, which could be a message or a bundle of messages.
func (h RegexpMatching) invoke(p Packet, exactMatch bool) error {
switch x := p.(type) {
case Message:
return h.Invoke(x, exactMatch)
case Bundle:
return h.immediately(x, exactMatch)
default:
return errors.Errorf("unsupported type for dispatcher: %T", p)
}
}

// Invoke invokes an OSC message.
func (h RegexpMatching) Invoke(msg Message, exactMatch bool) error {
for addressPattern, handler := range h {

re, err := regexp.Compile(addressPattern)
if err != nil {
return err
}

if re.MatchString(msg.Address) {
return handler.Handle(msg)
}
}
return nil
}
Loading