groupware: fix deserialization of Event Alert Trigger types using mapstructure

This commit is contained in:
Pascal Bleser
2025-11-05 16:21:47 +01:00
parent 48fcff45d0
commit c1cd614abc
3 changed files with 128 additions and 3 deletions

View File

@@ -725,3 +725,51 @@ func TestUnmarshallingCalendarEventGetResponse(t *testing.T) {
require.Equal("9a7ab91a-edca-4988-886f-25e00743430d", result.Uid)
require.Equal(jscalendar.PrivacyPublic, result.Privacy)
}
func TestAlertWithOffsetTriggerInResponse(t *testing.T) {
require := require.New(t)
text := `{
"methodResponses":[
["CalendarEvent/get",{
"accountId":"b",
"state":"ssecq",
"list":[{
"@type": "Event",
"start":"2025-11-01T14:30:00",
"alerts": {
"M87fT82": {
"@type": "Alert",
"trigger": {
"@type": "OffsetTrigger",
"offset": "-PT15M",
"relativeTo": "start"
}
}
}
}],
"notFound":[]
},"1"]
],"sessionState":"7d3cae5b"
}`
var response Response
err := json.Unmarshal([]byte(text), &response)
require.NoError(err)
resp := response.MethodResponses[0]
require.Equal(CommandCalendarEventGet, resp.Command)
require.IsType(CalendarEventGetResponse{}, resp.Parameters)
params := resp.Parameters.(CalendarEventGetResponse)
require.Len(params.List, 1)
event := params.List[0]
require.Contains(event.Alerts, "M87fT82")
alert := event.Alerts["M87fT82"]
require.NotNil(alert)
trigger := alert.Trigger
require.NotNil(trigger)
require.IsType(jscalendar.OffsetTrigger{}, trigger)
offsetTrigger := trigger.(jscalendar.OffsetTrigger)
require.Equal(jscalendar.SignedDuration("-PT15M"), offsetTrigger.Offset)
require.Equal(jscalendar.RelativeToStart, offsetTrigger.RelativeTo)
}

View File

@@ -12,6 +12,7 @@ import (
"time"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/opencloud/pkg/jscalendar"
"github.com/opencloud-eu/opencloud/pkg/log"
)
@@ -139,8 +140,9 @@ func mapstructStringToTimeHook() mapstructure.DecodeHookFunc {
// mapstruct isn't able to properly map RFC3339 date strings into Time
// objects, which is why we require this custom hook,
// see https://github.com/mitchellh/mapstructure/issues/41
wanted := reflect.TypeOf(time.Time{})
return func(from reflect.Type, to reflect.Type, data any) (any, error) {
if to != reflect.TypeOf(time.Time{}) {
if to != wanted {
return data, nil
}
switch from.Kind() {
@@ -159,8 +161,11 @@ func mapstructStringToTimeHook() mapstructure.DecodeHookFunc {
func decodeMap(input map[string]any, target any) error {
// https://github.com/mitchellh/mapstructure/issues/41
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
Metadata: nil,
DecodeHook: mapstructure.ComposeDecodeHookFunc(mapstructStringToTimeHook()),
Metadata: nil,
DecodeHook: mapstructure.ComposeDecodeHookFunc(
mapstructStringToTimeHook(),
jscalendar.MapstructTriggerHook(),
),
Result: &target,
ErrorUnused: false,
ErrorUnset: false,

View File

@@ -3,7 +3,11 @@ package jscalendar
import (
"encoding/json"
"fmt"
"reflect"
"slices"
"time"
"github.com/mitchellh/mapstructure"
)
// This is a date-time string with no time zone/offset information.
@@ -1407,6 +1411,74 @@ var _ Trigger = UnknownTrigger{}
func (o UnknownTrigger) trigger() {}
func MapstructTriggerHook() mapstructure.DecodeHookFunc {
fn := func(Trigger) {}
wanted := reflect.TypeOf(fn).In(0)
return func(from reflect.Type, to reflect.Type, data any) (any, error) {
if to != wanted {
return data, nil
}
m := data.(map[string]any)
if typ, ok := m["@type"]; ok {
switch typ {
case string(OffsetTriggerType):
return mapOffsetTrigger(m)
case string(AbsoluteTriggerType):
return mapAbsoluteTrigger(m)
default:
return UnknownTrigger(m), nil
}
} else {
if _, ok := m["offset"]; ok {
return mapOffsetTrigger(m)
}
if _, ok := m["when"]; ok {
return mapAbsoluteTrigger(m)
} else {
return UnknownTrigger(m), nil
}
}
}
}
func mapOffsetTrigger(m map[string]any) (OffsetTrigger, error) {
trigger := OffsetTrigger{
Type: OffsetTriggerType,
}
if value, ok := m["offset"]; ok {
if str, ok := value.(string); ok {
trigger.Offset = SignedDuration(str)
}
}
if value, ok := m["relativeTo"]; ok {
if str, ok := value.(string); ok {
t := RelativeTo(str)
if slices.Contains(RelativeTos, t) {
trigger.RelativeTo = t
} else {
return trigger, fmt.Errorf("unsupported Trigger.relativeTo value: '%v'", value)
}
}
}
return trigger, nil
}
func mapAbsoluteTrigger(m map[string]any) (AbsoluteTrigger, error) {
trigger := AbsoluteTrigger{
Type: AbsoluteTriggerType,
}
if value, ok := m["when"]; ok {
if str, ok := value.(string); ok {
if w, err := time.Parse(time.RFC3339, str); err != nil {
trigger.When = w
} else {
return trigger, err
}
}
}
return trigger, nil
}
type Alert struct {
// This specifies the type of this object. This MUST be `Alert`.
Type TypeOfAlert `json:"@type,omitempty"`