Skip to content

Commit b649b67

Browse files
authored
Implement UnmarshalJSON for FCM Types (#209)
* Implementing unmarshal logic * Implemented more unmarshallers * Added the rest of unmarshal logic * Updated changelog * Updated tests
1 parent 1474822 commit b649b67

File tree

4 files changed

+314
-49
lines changed

4 files changed

+314
-49
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Unreleased
22

33
- [added] `messaging.Aps` type now supports critical sound in its payload.
4+
- [fixed] Public types in the `messaging` package now support correct
5+
JSON marshalling and unmarshalling.
46

57
# v3.5.0
68

messaging/messaging.go

Lines changed: 213 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"fmt"
2424
"net/http"
2525
"regexp"
26+
"strconv"
2627
"strings"
2728
"time"
2829

@@ -149,16 +150,33 @@ type Message struct {
149150

150151
// MarshalJSON marshals a Message into JSON (for internal use only).
151152
func (m *Message) MarshalJSON() ([]byte, error) {
152-
// Create a new type to prevent infinite recursion.
153+
// Create a new type to prevent infinite recursion. We use this technique whenever it is needed
154+
// to customize how a subset of the fields in a struct should be serialized.
153155
type messageInternal Message
154-
s := &struct {
156+
temp := &struct {
155157
BareTopic string `json:"topic,omitempty"`
156158
*messageInternal
157159
}{
158160
BareTopic: strings.TrimPrefix(m.Topic, "/topics/"),
159161
messageInternal: (*messageInternal)(m),
160162
}
161-
return json.Marshal(s)
163+
return json.Marshal(temp)
164+
}
165+
166+
// UnmarshalJSON unmarshals a JSON string into a Message (for internal use only).
167+
func (m *Message) UnmarshalJSON(b []byte) error {
168+
type messageInternal Message
169+
s := struct {
170+
BareTopic string `json:"topic,omitempty"`
171+
*messageInternal
172+
}{
173+
messageInternal: (*messageInternal)(m),
174+
}
175+
if err := json.Unmarshal(b, &s); err != nil {
176+
return err
177+
}
178+
m.Topic = s.BareTopic
179+
return nil
162180
}
163181

164182
// Notification is the basic notification template to use across all platforms.
@@ -191,14 +209,48 @@ func (a *AndroidConfig) MarshalJSON() ([]byte, error) {
191209
}
192210

193211
type androidInternal AndroidConfig
194-
s := &struct {
212+
temp := &struct {
195213
TTL string `json:"ttl,omitempty"`
196214
*androidInternal
197215
}{
198216
TTL: ttl,
199217
androidInternal: (*androidInternal)(a),
200218
}
201-
return json.Marshal(s)
219+
return json.Marshal(temp)
220+
}
221+
222+
// UnmarshalJSON unmarshals a JSON string into an AndroidConfig (for internal use only).
223+
func (a *AndroidConfig) UnmarshalJSON(b []byte) error {
224+
type androidInternal AndroidConfig
225+
temp := struct {
226+
TTL string `json:"ttl,omitempty"`
227+
*androidInternal
228+
}{
229+
androidInternal: (*androidInternal)(a),
230+
}
231+
if err := json.Unmarshal(b, &temp); err != nil {
232+
return err
233+
}
234+
if temp.TTL != "" {
235+
segments := strings.Split(strings.TrimSuffix(temp.TTL, "s"), ".")
236+
if len(segments) != 1 && len(segments) != 2 {
237+
return fmt.Errorf("incorrect number of segments in ttl: %q", temp.TTL)
238+
}
239+
seconds, err := strconv.ParseInt(segments[0], 10, 64)
240+
if err != nil {
241+
return err
242+
}
243+
ttl := time.Duration(seconds) * time.Second
244+
if len(segments) == 2 {
245+
nanos, err := strconv.ParseInt(strings.TrimLeft(segments[1], "0"), 10, 64)
246+
if err != nil {
247+
return err
248+
}
249+
ttl += time.Duration(nanos) * time.Nanosecond
250+
}
251+
a.TTL = &ttl
252+
}
253+
return nil
202254
}
203255

204256
// AndroidNotification is a notification to send to Android devices.
@@ -240,30 +292,30 @@ type WebpushNotificationAction struct {
240292
// See https://developer.mozilla.org/en-US/docs/Web/API/notification/Notification for additional
241293
// details.
242294
type WebpushNotification struct {
243-
Actions []*WebpushNotificationAction
244-
Title string `json:"title,omitempty"` // if specified, overrides the Title field of the Notification type
245-
Body string `json:"body,omitempty"` // if specified, overrides the Body field of the Notification type
246-
Icon string `json:"icon,omitempty"`
247-
Badge string `json:"badge,omitempty"`
248-
Direction string `json:"dir,omitempty"` // one of 'ltr' or 'rtl'
249-
Data interface{} `json:"data,omitempty"`
250-
Image string `json:"image,omitempty"`
251-
Language string `json:"lang,omitempty"`
252-
Renotify bool `json:"renotify,omitempty"`
253-
RequireInteraction bool `json:"requireInteraction,omitempty"`
254-
Silent bool `json:"silent,omitempty"`
255-
Tag string `json:"tag,omitempty"`
256-
TimestampMillis *int64 `json:"timestamp,omitempty"`
257-
Vibrate []int `json:"vibrate,omitempty"`
295+
Actions []*WebpushNotificationAction `json:"actions,omitempty"`
296+
Title string `json:"title,omitempty"` // if specified, overrides the Title field of the Notification type
297+
Body string `json:"body,omitempty"` // if specified, overrides the Body field of the Notification type
298+
Icon string `json:"icon,omitempty"`
299+
Badge string `json:"badge,omitempty"`
300+
Direction string `json:"dir,omitempty"` // one of 'ltr' or 'rtl'
301+
Data interface{} `json:"data,omitempty"`
302+
Image string `json:"image,omitempty"`
303+
Language string `json:"lang,omitempty"`
304+
Renotify bool `json:"renotify,omitempty"`
305+
RequireInteraction bool `json:"requireInteraction,omitempty"`
306+
Silent bool `json:"silent,omitempty"`
307+
Tag string `json:"tag,omitempty"`
308+
TimestampMillis *int64 `json:"timestamp,omitempty"`
309+
Vibrate []int `json:"vibrate,omitempty"`
258310
CustomData map[string]interface{}
259311
}
260312

261-
// WebpushFcmOptions Options for features provided by the FCM SDK for Web.
262-
type WebpushFcmOptions struct {
263-
Link string `json:"link,omitempty"`
264-
}
265-
266313
// standardFields creates a map containing all the fields except the custom data.
314+
//
315+
// We implement a standardFields function whenever we want to add custom and arbitrary
316+
// fields to an object during its serialization. This helper function also comes in
317+
// handy during validation of the message (to detect duplicate specifications of
318+
// fields), and also during deserialization.
267319
func (n *WebpushNotification) standardFields() map[string]interface{} {
268320
m := make(map[string]interface{})
269321
addNonEmpty := func(key, value string) {
@@ -311,6 +363,31 @@ func (n *WebpushNotification) MarshalJSON() ([]byte, error) {
311363
return json.Marshal(m)
312364
}
313365

366+
// UnmarshalJSON unmarshals a JSON string into a WebpushNotification (for internal use only).
367+
func (n *WebpushNotification) UnmarshalJSON(b []byte) error {
368+
type webpushNotificationInternal WebpushNotification
369+
var temp = (*webpushNotificationInternal)(n)
370+
if err := json.Unmarshal(b, temp); err != nil {
371+
return err
372+
}
373+
allFields := make(map[string]interface{})
374+
if err := json.Unmarshal(b, &allFields); err != nil {
375+
return err
376+
}
377+
for k := range n.standardFields() {
378+
delete(allFields, k)
379+
}
380+
if len(allFields) > 0 {
381+
n.CustomData = allFields
382+
}
383+
return nil
384+
}
385+
386+
// WebpushFcmOptions contains additional options for features provided by the FCM web SDK.
387+
type WebpushFcmOptions struct {
388+
Link string `json:"link,omitempty"`
389+
}
390+
314391
// APNSConfig contains messaging options specific to the Apple Push Notification Service (APNS).
315392
//
316393
// See https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html
@@ -328,34 +405,59 @@ type APNSConfig struct {
328405
// See https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html
329406
// for a full list of supported payload fields.
330407
type APNSPayload struct {
331-
Aps *Aps
332-
CustomData map[string]interface{}
408+
Aps *Aps `json:"aps,omitempty"`
409+
CustomData map[string]interface{} `json:"-"`
410+
}
411+
412+
// standardFields creates a map containing all the fields except the custom data.
413+
func (p *APNSPayload) standardFields() map[string]interface{} {
414+
return map[string]interface{}{"aps": p.Aps}
333415
}
334416

335417
// MarshalJSON marshals an APNSPayload into JSON (for internal use only).
336418
func (p *APNSPayload) MarshalJSON() ([]byte, error) {
337-
m := map[string]interface{}{"aps": p.Aps}
419+
m := p.standardFields()
338420
for k, v := range p.CustomData {
339421
m[k] = v
340422
}
341423
return json.Marshal(m)
342424
}
343425

426+
// UnmarshalJSON unmarshals a JSON string into an APNSPayload (for internal use only).
427+
func (p *APNSPayload) UnmarshalJSON(b []byte) error {
428+
type apnsPayloadInternal APNSPayload
429+
var temp = (*apnsPayloadInternal)(p)
430+
if err := json.Unmarshal(b, temp); err != nil {
431+
return err
432+
}
433+
allFields := make(map[string]interface{})
434+
if err := json.Unmarshal(b, &allFields); err != nil {
435+
return err
436+
}
437+
for k := range p.standardFields() {
438+
delete(allFields, k)
439+
}
440+
if len(allFields) > 0 {
441+
p.CustomData = allFields
442+
}
443+
return nil
444+
}
445+
344446
// Aps represents the aps dictionary that may be included in an APNSPayload.
345447
//
346448
// Alert may be specified as a string (via the AlertString field), or as a struct (via the Alert
347449
// field).
348450
type Aps struct {
349-
AlertString string
350-
Alert *ApsAlert
351-
Badge *int
352-
Sound string
353-
CriticalSound *CriticalSound
354-
ContentAvailable bool
355-
MutableContent bool
356-
Category string
357-
ThreadID string
358-
CustomData map[string]interface{}
451+
AlertString string `json:"-"`
452+
Alert *ApsAlert `json:"-"`
453+
Badge *int `json:"badge,omitempty"`
454+
Sound string `json:"-"`
455+
CriticalSound *CriticalSound `json:"-"`
456+
ContentAvailable bool `json:"-"`
457+
MutableContent bool `json:"-"`
458+
Category string `json:"category,omitempty"`
459+
ThreadID string `json:"thread-id,omitempty"`
460+
CustomData map[string]interface{} `json:"-"`
359461
}
360462

361463
// standardFields creates a map containing all the fields except the custom data.
@@ -398,26 +500,89 @@ func (a *Aps) MarshalJSON() ([]byte, error) {
398500
return json.Marshal(m)
399501
}
400502

503+
// UnmarshalJSON unmarshals a JSON string into an Aps (for internal use only).
504+
func (a *Aps) UnmarshalJSON(b []byte) error {
505+
type apsInternal Aps
506+
temp := struct {
507+
AlertObject *json.RawMessage `json:"alert,omitempty"`
508+
SoundObject *json.RawMessage `json:"sound,omitempty"`
509+
ContentAvailableInt int `json:"content-available,omitempty"`
510+
MutableContentInt int `json:"mutable-content,omitempty"`
511+
*apsInternal
512+
}{
513+
apsInternal: (*apsInternal)(a),
514+
}
515+
if err := json.Unmarshal(b, &temp); err != nil {
516+
return err
517+
}
518+
a.ContentAvailable = (temp.ContentAvailableInt == 1)
519+
a.MutableContent = (temp.MutableContentInt == 1)
520+
if temp.AlertObject != nil {
521+
if err := json.Unmarshal(*temp.AlertObject, &a.Alert); err != nil {
522+
a.Alert = nil
523+
if err := json.Unmarshal(*temp.AlertObject, &a.AlertString); err != nil {
524+
return fmt.Errorf("failed to unmarshal alert as a struct or a string: %v", err)
525+
}
526+
}
527+
}
528+
if temp.SoundObject != nil {
529+
if err := json.Unmarshal(*temp.SoundObject, &a.CriticalSound); err != nil {
530+
a.CriticalSound = nil
531+
if err := json.Unmarshal(*temp.SoundObject, &a.Sound); err != nil {
532+
return fmt.Errorf("failed to unmarshal sound as a struct or a string")
533+
}
534+
}
535+
}
536+
537+
allFields := make(map[string]interface{})
538+
if err := json.Unmarshal(b, &allFields); err != nil {
539+
return err
540+
}
541+
for k := range a.standardFields() {
542+
delete(allFields, k)
543+
}
544+
if len(allFields) > 0 {
545+
a.CustomData = allFields
546+
}
547+
return nil
548+
}
549+
401550
// CriticalSound is the sound payload that can be included in an Aps.
402551
type CriticalSound struct {
403-
Critical bool
404-
Name string
405-
Volume float64
552+
Critical bool `json:"-"`
553+
Name string `json:"name,omitempty"`
554+
Volume float64 `json:"volume,omitempty"`
406555
}
407556

408557
// MarshalJSON marshals a CriticalSound into JSON (for internal use only).
409558
func (cs *CriticalSound) MarshalJSON() ([]byte, error) {
410-
m := make(map[string]interface{})
559+
type criticalSoundInternal CriticalSound
560+
temp := struct {
561+
CriticalInt int `json:"critical,omitempty"`
562+
*criticalSoundInternal
563+
}{
564+
criticalSoundInternal: (*criticalSoundInternal)(cs),
565+
}
411566
if cs.Critical {
412-
m["critical"] = 1
567+
temp.CriticalInt = 1
413568
}
414-
if cs.Name != "" {
415-
m["name"] = cs.Name
569+
return json.Marshal(temp)
570+
}
571+
572+
// UnmarshalJSON unmarshals a JSON string into a CriticalSound (for internal use only).
573+
func (cs *CriticalSound) UnmarshalJSON(b []byte) error {
574+
type criticalSoundInternal CriticalSound
575+
temp := struct {
576+
CriticalInt int `json:"critical,omitempty"`
577+
*criticalSoundInternal
578+
}{
579+
criticalSoundInternal: (*criticalSoundInternal)(cs),
416580
}
417-
if cs.Volume != 0 {
418-
m["volume"] = cs.Volume
581+
if err := json.Unmarshal(b, &temp); err != nil {
582+
return err
419583
}
420-
return json.Marshal(m)
584+
cs.Critical = (temp.CriticalInt == 1)
585+
return nil
421586
}
422587

423588
// ApsAlert is the alert payload that can be included in an Aps.

0 commit comments

Comments
 (0)