diff --git a/events.go b/events.go new file mode 100644 index 0000000..4d74d86 --- /dev/null +++ b/events.go @@ -0,0 +1,237 @@ +package async + +import ( + "reflect" +) + +/* + + Event is a map of functions to use for a single event. The integer is the + frequency of calls that the function should make. + +*/ +type Event map[reflect.Value]int + +/* + + List is used for containing everything related to the events. It stores the + name of the event, functions for the event, and number of times the function + should be called when the event is triggered. + + All you need to do to create an event list is: + events := make(async.Events) + + All event commands can be chained together. For example: + events.On("myevent", func() { + println("Called myevent") + }).On("myevent2", func(msg string) { + fmt.Printf("Called myevent2 with message: %s\n", msg) + }).Emit("myevent").Emit("myevent2", "Testing") + + You can return an error from a function and it will be emitted as an error + event. For example: + events.On("error", func(err error) { + fmt.Printf("Error: %s", err) + }).On("myevent", func() error { + return fmt.Errorf("Some error message") + }).Emit("myevent") + + + It's also easily inheritable by other structures. For example: + type MyStruct struct { + Events + } + + m := MyStruct{make(async.Events)} + m.On("myevent", func() { + println("Called myevent") + }).Emit("myevent") + +*/ +type Events map[string]Event + +/* + + Clear all events out of the event list. + + Returns the list of events for chaining commands. + +*/ +func (e Events) Clear() Events { + for key := range e { + delete(e, key) + } + return e +} + +/* + + Emit an event. Arguments are optional. Each event will be ran as a Series. + + For example: + events := make(async.Events) + events.On("myevent", func() { + println("Emitted myevent") + }) + events.Emit("myevent") + + With arguments: + events := make(async.Events) + events.On("myevent", func(msg string) { + fmt.Printf("Message: %s\n", msg) + }) + events.Emit("myevent", "Testing") + + Returns the list of events for chaining commands. + +*/ +func (e Events) Emit(name string, args ...interface{}) Events { + var ( + routines = make([]Routine, 0) + values = make([]reflect.Value, 0) + ) + + // If we don't have any events with this name, simply return. + if e.Get(name) == nil { + return e + } + + // Reflect all of our arguments for the reflect.Value.Call + for i := 0; i < len(args); i++ { + values = append(values, reflect.ValueOf(args[i])) + } + + for fn, freq := range e[name] { + // Decrease frequency + if freq > 0 { + freq-- + } + + // If the frequency is down to 0, remove the callback from the event + // so that it isn't triggered again. + if freq == 0 { + delete(e[name], fn) + } else { + e[name][fn] = freq + } + + // Delete the entire event if all callbacks have been triggered + if len(e[name]) == 0 { + delete(e, name) + } + + // Create the routines to pass into Series + routines = append(routines, + func(e Events, fn reflect.Value, values []reflect.Value) Routine { + return func(done Done, args ...interface{}) { + values := fn.Call(values) + for i := 0; i < len(values); i++ { + v := values[i].Interface() + switch v.(type) { + case error: + done(v.(error)) + return + } + } + done(nil) + } + }(e, fn, values), + ) + } + + // Run all of the events in Series + Series(routines, func(err error, args ...interface{}) { + // Only emit the error event if an error was detected. Nothing else needs + // to be done here. + if err != nil { + e.Emit("error", err) + } + }) + + return e +} + +/* + + Get Event map of functions and frequencies for the named event. This is just + a convenience function. This data could also be accessed by the normal + mapping methods. + + For instance: + fmt.Printf("Events for myevent: %+v\n", e["myevent"]) + + Returns the list of events for chaining commands. + +*/ +func (e Events) Get(name string) Event { + return e[name] +} + +/* + + Get length of the Event map of functions and frequencies for the named + event. This is just a convenience function. This data could also be accessed + by the normal mapping methods. + + For instance: + fmt.Printf("Length: %d", len(e["myevent"])) + + Returns the list of events for chaining commands. + +*/ +func (e Events) Length(name string) int { + return len(e.Get(name)) +} + +/* + + Add an event to be called forever. + + This is equal to calling Times with -1 as the number of times to run the + event. More documentation can be found on the Times function. + + Returns the list of events for chaining commands. + +*/ +func (e Events) On(name string, callbacks ...interface{}) Events { + return e.Times(name, -1, callbacks...) +} + +/* + + Add an event to be called once. + + This is equal to calling Times with 1 as the number of times to run the + event. More documentation can be found on the Times function. + + Returns the list of events for chaining commands. + +*/ +func (e Events) Once(name string, callbacks ...interface{}) Events { + return e.Times(name, 1, callbacks...) +} + +/* + + Add an event to be called a number of times. If the number of times for the + function to be called is -1, it will be called until the list is cleared. + + Returns the list of events for chaining commands. + +*/ +func (e Events) Times(name string, times int, callbacks ...interface{}) Events { + // Check to see if the event already exists. If not, create its map. + if e[name] == nil { + e[name] = make(Event) + } + + for i := 0; i < len(callbacks); i++ { + // Reflect the function so that we don't have to add function restraints. + fn := reflect.ValueOf(callbacks[i]) + + // Set the number of times that the event should run. + e[name][fn] = times + } + + return e +} diff --git a/events_test.go b/events_test.go new file mode 100644 index 0000000..e6d013f --- /dev/null +++ b/events_test.go @@ -0,0 +1,325 @@ +package async_test + +import ( + "fmt" + "github.com/Southern/async" + "testing" + "time" +) + +var events = make(async.Events) + +func TestEventEmit(t *testing.T) { + Status("Try nonexistant event") + events.Emit("test") +} + +func TestEventClear(t *testing.T) { + Status("Add event") + events.On("test", func() { + Status("Called test") + }) + + Status("Clear list") + events.Clear() + + if len(events) > 0 { + t.Errorf("List wasn't cleared") + return + } +} + +func TestEventOnWithoutArguments(t *testing.T) { + Status("Clear list") + events.Clear() + + if len(events) > 0 { + t.Errorf("List wasn't cleared") + return + } + + Status("Add event") + events.On("test", + func() { + Status("Hit first callback") + }, + func() { + Status("Hit first callback") + }, + ) + + if events.Length("test") != 2 { + t.Errorf("Not all callbacks were added") + return + } + + Status("Emitting event") + events.Emit("test") + + if events.Length("test") != 2 { + t.Errorf("One or more callbacks were removed") + return + } +} + +func TestEventOnceWithoutArguments(t *testing.T) { + Status("Clear list") + events.Clear() + + if len(events) > 0 { + t.Errorf("List wasn't cleared") + return + } + + Status("Add event") + events.Once("test", + func() { + Status("Hit first callback") + }, + func() { + Status("Hit second callback") + }, + ) + + if events.Length("test") != 2 { + t.Errorf("Not all callbacks were added") + return + } + + Status("Emitting event") + events.Emit("test") + + if events.Length("test") != 0 { + t.Errorf("Not all callbacks were removed") + return + } +} + +func TestEventOnWithArguments(t *testing.T) { + Status("Clear list") + events.Clear() + + if len(events) > 0 { + t.Errorf("List wasn't cleared") + return + } + + Status("Add event") + events.On("test", + func(msg string) { + Status("Got message: %s", msg) + }, + ) + + if events.Length("test") != 1 { + t.Errorf("Not all callbacks were added") + return + } + + Status("Emitting event") + events.Emit("test", "blah") + + if events.Length("test") != 1 { + t.Errorf("One or more callbacks were removed") + return + } +} + +func TestEventOnceWithArguments(t *testing.T) { + Status("Clear list") + events.Clear() + + if len(events) > 0 { + t.Errorf("List wasn't cleared") + return + } + + Status("Add event") + events.Once("test", + func(msg string) { + Status("Got message: %s", msg) + }, + ) + + if events.Length("test") != 1 { + t.Errorf("Not all callbacks were added") + return + } + + Status("Emitting event") + events.Emit("test", "blah") + + if events.Length("test") != 0 { + t.Errorf("Not all callbacks were removed") + return + } +} + +func TestEventMix(t *testing.T) { + Status("Clear list") + events.Clear() + + if len(events) > 0 { + t.Errorf("List wasn't cleared") + return + } + + Status("Add event") + events.On("test", + func() { + Status("Hit first callback") + }, + ).Once("test", + func() { + Status("Hit second callback") + }, + ) + + if events.Length("test") != 2 { + t.Errorf("Not all callbacks were added") + return + } + + Status("Emitting event") + events.Emit("test") + + if events.Length("test") != 1 { + t.Errorf("Callback should have been removed but wasn't") + return + } +} + +func TestEventMixWithArguments(t *testing.T) { + Status("Clear list") + events.Clear() + + if len(events) > 0 { + t.Errorf("List wasn't cleared") + return + } + + Status("Add event") + events.On("test", + func(msg string) { + Status("Hit first callback; msg: %s", msg) + }, + ).Once("test", + func(msg string) { + Status("Hit second callback; msg: %s", msg) + }, + ) + + if events.Length("test") != 2 { + t.Errorf("Not all callbacks were added") + return + } + + Status("Emitting event") + events.Emit("test", "Testing") + + if events.Length("test") != 1 { + t.Errorf("Callback should have been removed but wasn't") + return + } +} + +func TestEventErrorReturn(t *testing.T) { + var _err error + + Status("Clear list") + events.Clear() + + if len(events) > 0 { + t.Errorf("List wasn't cleared") + return + } + + Status("Add event") + events.On("test", + func() error { + return fmt.Errorf("Testing") + }, + ).On("error", + func(err error) { + Status("Got error: %s", err) + _err = err + }, + ) + + if events.Length("test") != 1 { + t.Errorf("Not all callbacks were added") + return + } + + Status("Emitting event") + events.Emit("test") + + if events.Length("test") != 1 { + t.Errorf("One or more callbacks were removed") + return + } + + Status("Giving the error time to trigger") + time.Sleep(time.Second) + + if _err == nil { + t.Errorf("Expected an error") + return + } +} + +type MyStruct struct { + async.Events +} + +func TestEventInheritanceWithoutArguments(t *testing.T) { + Status("Creating struct") + mystruct := MyStruct{make(async.Events)} + + Status("Adding event") + mystruct.On("test", func() { + Status("Hit callback") + }) + + Status("Emitting event") + mystruct.Emit("test") +} + +func TestEventInheritanceWithArguments(t *testing.T) { + Status("Creating struct") + mystruct := MyStruct{make(async.Events)} + + Status("Adding event") + mystruct.On("test", func(msg string) { + Status("Hit callback with message: %s", msg) + }) + + Status("Emitting event") + mystruct.Emit("test", "Testing") +} + +func TestEventInheritanceErrorReturn(t *testing.T) { + var _err error + + Status("Creating struct") + mystruct := MyStruct{make(async.Events)} + + Status("Adding event") + mystruct.On("error", func(err error) { + _err = err + Status("Got error: %s", err) + }).On("test", func(msg string) error { + return fmt.Errorf("Testing") + }) + + Status("Emitting event") + mystruct.Emit("test", "Testing") + + Status("Giving the error time to trigger") + time.Sleep(time.Second) + + if _err == nil { + t.Errorf("Expected an error") + return + } +}