diff --git a/dispatcher.go b/dispatcher.go index bb6602f..c25ef90 100644 --- a/dispatcher.go +++ b/dispatcher.go @@ -1,9 +1,6 @@ package osc import ( - "strings" - "time" - "github.com/pkg/errors" ) @@ -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 -} diff --git a/message.go b/message.go index eace8b6..2c9c7b8 100644 --- a/message.go +++ b/message.go @@ -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. diff --git a/message_test.go b/message_test.go index 21dea60..b38e324 100644 --- a/message_test.go +++ b/message_test.go @@ -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) } @@ -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") } } diff --git a/pattern_matching.go b/pattern_matching.go new file mode 100644 index 0000000..ab1e4d4 --- /dev/null +++ b/pattern_matching.go @@ -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 +} diff --git a/dispatcher_test.go b/pattern_matching_test.go similarity index 84% rename from dispatcher_test.go rename to pattern_matching_test.go index 10ce5d6..e9ce829 100644 --- a/dispatcher_test.go +++ b/pattern_matching_test.go @@ -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 { @@ -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") @@ -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 { @@ -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 @@ -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") @@ -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) diff --git a/regexp_matching.go b/regexp_matching.go new file mode 100644 index 0000000..14c012c --- /dev/null +++ b/regexp_matching.go @@ -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 +} diff --git a/regexp_matching_test.go b/regexp_matching_test.go new file mode 100644 index 0000000..b8531b6 --- /dev/null +++ b/regexp_matching_test.go @@ -0,0 +1,171 @@ +package osc + +import ( + "errors" + "testing" + "time" +) + +var matchedError = errors.New("matched") + +// Test a successful method invocation. +func TestRegexpMatchingDispatchOK(t *testing.T) { + d := RegexpMatching{ + "/bar": Method(func(msg Message) error { + return matchedError + }), + "/foo.*": Method(func(msg Message) error { + return matchedError + }), + } + later := time.Now().Add(20 * time.Millisecond) + b := Bundle{ + Timetag: FromTime(later), + Packets: []Packet{ + Message{Address: "/bar"}, + }, + } + + if err := d.Dispatch(b, false); !errors.Is(err, matchedError) { + t.Fatalf("expected match, got: %v", err) + } + + b = Bundle{ + Packets: []Packet{ + Message{Address: "/foo"}, + }, + } + if err := d.Dispatch(b, false); !errors.Is(err, matchedError) { + t.Fatalf("expected match, got: %v", err) + } + b = Bundle{ + Packets: []Packet{ + Message{Address: "/foobar"}, + }, + } + if err := d.Dispatch(b, false); !errors.Is(err, matchedError) { + t.Fatalf("expected match, got: %v", err) + } + +} + +// Test a method that returns an error. +func TestRegexpMatchingDispatchError(t *testing.T) { + d := RegexpMatching{ + "/foo": Method(func(msg Message) error { + return matchedError + }), + "^/baz$": Method(func(msg Message) error { + return matchedError + }), + } + later := time.Now().Add(20 * time.Millisecond) + b := Bundle{ + Timetag: FromTime(later), + Packets: []Packet{ + Message{Address: "/foo"}, + }, + } + if err := d.Dispatch(b, false); !errors.Is(err, matchedError) { + t.Fatalf("expected match, got: %v", err) + } + b = Bundle{ + Timetag: FromTime(later), + Packets: []Packet{ + Message{Address: "all matches /foo if it contains /foo"}, + }, + } + if err := d.Dispatch(b, false); !errors.Is(err, matchedError) { + t.Fatalf("expected match, got: %v", err) + } + + b = Bundle{ + Timetag: FromTime(later), + Packets: []Packet{ + Message{Address: "/baz"}, + }, + } + if err := d.Dispatch(b, false); !errors.Is(err, matchedError) { + t.Fatalf("expected match, got: %v", err) + } + + later = time.Now().Add(20 * time.Millisecond) + b = Bundle{ + Timetag: FromTime(later), + Packets: []Packet{ + Message{Address: "/bazbaz"}, + }, + } + if err := d.Dispatch(b, false); err != nil { + t.Fatalf("expected no error, got: %v", err) + } +} + +func TestRegexpMatchingDispatchNestedBundle(t *testing.T) { + c := make(chan struct{}) + d := RegexpMatching{ + "/foo": Method(func(msg Message) error { + close(c) + return nil + }), + } + later := time.Now().Add(20 * time.Millisecond) + b := Bundle{ + Timetag: FromTime(later), + Packets: []Packet{ + Bundle{ + Timetag: FromTime(later.Add(20 * time.Millisecond)), + Packets: []Packet{ + Message{Address: "/foo"}, + }, + }, + }, + } + if err := d.Dispatch(b, false); err != nil { + t.Fatal(err) + } + <-c +} + +func TestRegexpMatchingMiss(t *testing.T) { + d := RegexpMatching{ + "/foo": Method(func(msg Message) error { + return nil + }), + } + b := Bundle{ + Timetag: FromTime(time.Now()), + } + if err := d.Dispatch(b, false); err != nil { + t.Fatal(err) + } +} + +func TestRegexpMatchingInvoke(t *testing.T) { + d := RegexpMatching{ + "/foo": Method(func(msg Message) error { + return matchedError + }), + "/bar": Method(func(msg Message) error { + return nil + }), + } + msg := Message{Address: "/foo"} + if err := d.Invoke(msg, false); !errors.Is(err, matchedError) { + t.Fatal("expected matched error, got: %w", err) + } + badMsg := Message{Address: "/["} + if err := d.Invoke(badMsg, false); err != nil { + t.Fatal("expected nil, got: %w", err) + } + if err := d.Invoke(Message{Address: "/bar"}, false); err != nil { + t.Fatal("expected nil, got: %w", err) + } + if err := d.Invoke(Message{Address: "/baz"}, false); err != nil { + t.Fatal("expected nil, got: %w", err) + } + + if err := d.invoke(badPacket{}, false); err == nil { + t.Fatal("expected no error, got: %w", err) + } +}