diff --git a/pubsub/pubsub.go b/pubsub/pubsub.go index 8a8d059..63940e7 100644 --- a/pubsub/pubsub.go +++ b/pubsub/pubsub.go @@ -16,12 +16,13 @@ import ( // code modeled after https://github.com/purposeinplay/go-commons/blob/v0.6.2/pubsub/inmem/pubsub.go type Event struct { - ID *string `json:"id"` - State eventstate.Type `json:"state"` - Target *string `json:"target"` - Detail any `json:"detail"` - SessionID *string `json:"session_id"` - ElementKey *string `json:"element_key"` + ID *string `json:"id"` + State eventstate.Type `json:"state"` + Target *string `json:"target"` + Detail any `json:"detail"` + StateDetail any `json:"state_detail"` + SessionID *string `json:"session_id"` + ElementKey *string `json:"element_key"` } // Subscription is a subscription to a channel. diff --git a/render.go b/render.go index c9119b9..c472d85 100644 --- a/render.go +++ b/render.go @@ -57,7 +57,7 @@ func buildDOMEventFromTemplate(ctx RouteContext, pubsubEvent pubsub.Event, event Type: eventType, Key: pubsubEvent.ElementKey, Target: targetOrClassName(pubsubEvent.Target, getClassName(*eventType)), - Detail: pubsubEvent.Detail, + Detail: pubsubEvent.StateDetail, } } eventType := fir(eventIDWithState, templateName) diff --git a/route.go b/route.go index 8b40f04..310ff7b 100644 --- a/route.go +++ b/route.go @@ -152,13 +152,6 @@ func OnEvent(name string, onEventFunc OnEventFunc) RouteOption { } } -type routeData map[string]any - -func (r *routeData) Error() string { - b, _ := json.Marshal(r) - return string(b) -} - type routeRenderer func(data routeData) error type eventPublisher func(event pubsub.Event) error type routeOpt struct { @@ -442,16 +435,37 @@ func handleOnEventResult(err error, ctx RouteContext, publish eventPublisher) { }) return case *routeData: - data := *errVal publish(pubsub.Event{ ID: &ctx.event.ID, State: eventstate.OK, Target: &target, ElementKey: ctx.event.ElementKey, - Detail: data, + Detail: *errVal, SessionID: ctx.event.SessionID, }) return + + case *routeDataWithState: + publish(pubsub.Event{ + ID: &ctx.event.ID, + State: eventstate.OK, + Target: &target, + ElementKey: ctx.event.ElementKey, + Detail: *errVal.routeData, + StateDetail: *errVal.stateData, + SessionID: ctx.event.SessionID, + }) + return + case *stateData: + publish(pubsub.Event{ + ID: &ctx.event.ID, + State: eventstate.OK, + Target: &target, + ElementKey: ctx.event.ElementKey, + StateDetail: *errVal, + SessionID: ctx.event.SessionID, + }) + return default: errs := map[string]any{ ctx.event.ID: firErrors.User(err).Error(), @@ -476,7 +490,7 @@ func handlePostFormResult(err error, ctx RouteContext) { } switch err.(type) { - case *routeData: + case *routeData, *stateData, *routeDataWithState: http.Redirect(ctx.response, ctx.request, ctx.request.URL.Path, http.StatusFound) default: handleOnLoadResult(ctx.route.onLoad(ctx), err, ctx) @@ -522,6 +536,24 @@ func handleOnLoadResult(err, onFormErr error, ctx RouteContext) { onLoadData["fir"] = newRouteDOMContext(ctx, errs) renderRoute(ctx, false)(onLoadData) + case *routeDataWithState: + onLoadData := *errVal.routeData + errs := make(map[string]any) + if onFormErr != nil { + fieldErrorsVal, ok := onFormErr.(*firErrors.Fields) + if !ok { + errs = map[string]any{ + ctx.event.ID: onFormErr.Error(), + } + } else { + errs = map[string]any{ + ctx.event.ID: fieldErrorsVal.Map(), + } + } + } + onLoadData["fir"] = newRouteDOMContext(ctx, errs) + renderRoute(ctx, false)(onLoadData) + case firErrors.Status: errs := make(map[string]any) if onFormErr != nil { diff --git a/route_context.go b/route_context.go index aba5117..016c31b 100644 --- a/route_context.go +++ b/route_context.go @@ -158,7 +158,19 @@ func (c RouteContext) Redirect(url string, status int) error { // KV is a wrapper for ctx.Data(map[string]any{key: data}) func (c RouteContext) KV(key string, data any) error { - return c.Data(map[string]any{key: data}) + return buildData(false, map[string]any{key: data}) +} + +// KV is a wrapper for ctx.State(map[string]any{key: data}) +func (c RouteContext) StateKV(key string, data any) error { + return buildData(true, map[string]any{key: data}) +} + +// State data is only passed to event receiver without a bound template +// it can be acccessed in the event receiver via $event.detail +// e.g. @fir:myevent:ok="console.log('$event.detail.mykey')" +func (c RouteContext) State(dataset ...any) error { + return buildData(true, dataset...) } // Data sets the data to be hydrated into the route's template or an event's associated template/block action @@ -170,38 +182,7 @@ func (c RouteContext) KV(key string, data any) error { // The function will return nil if no data is passed // The function accepts variadic arguments so that you can pass multiple structs or maps which will be merged func (c RouteContext) Data(dataset ...any) error { - if len(dataset) == 0 { - return nil - } - m := routeData{} - for _, data := range dataset { - val := reflect.ValueOf(data) - if val.Kind() == reflect.Ptr { - el := val.Elem() // dereference the pointer - if el.Kind() == reflect.Struct { - for k, v := range structs.Map(data) { - m[k] = v - } - } - } else if val.Kind() == reflect.Struct { - for k, v := range structs.Map(data) { - m[k] = v - } - } else if val.Kind() == reflect.Map { - ms, ok := data.(map[string]any) - if !ok { - return errors.New("data must be a map[string]any , struct or pointer to a struct") - } - - for k, v := range ms { - m[k] = v - } - } else { - return errors.New("data must be a map[string]any , struct or pointer to a struct") - } - } - - return &m + return buildData(false, dataset...) } // FieldError sets the error message for the given field and can be looked up by {{.fir.Error "myevent.field"}} diff --git a/route_data.go b/route_data.go new file mode 100644 index 0000000..0d032fa --- /dev/null +++ b/route_data.go @@ -0,0 +1,100 @@ +package fir + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" + + "github.com/fatih/structs" +) + +type routeData map[string]any + +func (r *routeData) Error() string { + b, _ := json.Marshal(r) + return string(b) +} + +type stateData map[string]any + +func (r *stateData) Error() string { + b, _ := json.Marshal(r) + return string(b) +} + +type routeDataWithState struct { + routeData *routeData + stateData *stateData +} + +func (r *routeDataWithState) Error() string { + b1, _ := json.Marshal(r.routeData) + b2, _ := json.Marshal(r.stateData) + return fmt.Sprintf("routeData: %s\n stateData: %s", string(b1), string(b2)) +} + +func buildData(stateOnly bool, dataset ...any) error { + if len(dataset) == 0 { + return nil + } + + m := make(map[string]any) + hasState := false + state := make(stateData) + + for _, data := range dataset { + if data == nil { + continue + } + if sv, ok := data.(*stateData); ok { + hasState = true + for k, v := range *sv { + state[k] = v + m[k] = v + } + } + val := reflect.ValueOf(data) + + if val.Kind() == reflect.Ptr { + el := val.Elem() // dereference the pointer + if el.Kind() == reflect.Struct { + for k, v := range structs.Map(data) { + m[k] = v + } + } + } else if val.Kind() == reflect.Struct { + for k, v := range structs.Map(data) { + m[k] = v + } + } else if val.Kind() == reflect.Map { + ms, ok := data.(map[string]any) + if !ok { + return errors.New("data must be a map[string]any , struct or pointer to a struct") + } + + for k, v := range ms { + m[k] = v + } + } else { + return errors.New("data must be a map[string]any , struct or pointer to a struct") + } + } + + if stateOnly { + t := stateData(m) + return &t + } + + if hasState { + r := routeData(m) + t := routeDataWithState{ + routeData: &r, + stateData: &state, + } + return &t + } + + r := routeData(m) + return &r +} diff --git a/route_data_test.go b/route_data_test.go new file mode 100644 index 0000000..95bb35d --- /dev/null +++ b/route_data_test.go @@ -0,0 +1,64 @@ +package fir + +import ( + "reflect" + "testing" +) + +type TestData struct { + Name string + Age int +} + +func TestBuildData(t *testing.T) { + // Test case 1: No dataset provided + err := buildData(false) + if err != nil { + t.Errorf("Expected nil error, got: %v", err) + } + + // Test case 2: Only routeData provided + data := map[string]any{"key": "value"} + err = buildData(false, data) + if err == nil { + t.Errorf("Expected error, got: %v", err) + } + r, ok := err.(*routeData) + if !ok { + t.Errorf("Expected error type *routeData, got: %v", reflect.TypeOf(err)) + } + if !reflect.DeepEqual(*r, routeData{"key": "value"}) { + t.Errorf("Expected error value %v, got: %v", data, *r) + } + + // Test case 2: Only stateData provided + err = buildData(true, data) + if err == nil { + t.Errorf("Expected error, got: %v", err) + } + s, ok := err.(*stateData) + if !ok { + t.Errorf("Expected error type *stateData, got: %v", reflect.TypeOf(err)) + } + if !reflect.DeepEqual(*s, stateData{"key": "value"}) { + t.Errorf("Expected error value %v, got: %v", data, *s) + } + + // Test case 3: Both routeData and stateData provided + err = buildData(false, data, buildData(true, map[string]any{"key1": "value1"})) + if err == nil { + t.Errorf("Expected error, got: %v", err) + } + rs, ok := err.(*routeDataWithState) + if !ok { + t.Errorf("Expected error type *routeDataWithState, got: %v", reflect.TypeOf(err)) + } + expectedRouteData := routeData{"key": "value", "key1": "value1"} + if !reflect.DeepEqual(*rs.routeData, expectedRouteData) { + t.Errorf("Expected error value %v, got: %v", expectedRouteData, *rs.routeData) + } + if !reflect.DeepEqual(*rs.stateData, stateData{"key1": "value1"}) { + t.Errorf("Expected error value %v, got: %v", data, *rs.stateData) + } + +}