Skip to content

Commit 3d4dcbe

Browse files
committed
feat: add marshal function
Add Marshal function, for now, only supporting structs.
1 parent 446849d commit 3d4dcbe

File tree

4 files changed

+155
-5
lines changed

4 files changed

+155
-5
lines changed

errors.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package vcard
2+
3+
import "errors"
4+
5+
var (
6+
// ErrUnsupportedType indicates that an unsupported interface type was sent for conversion.
7+
ErrUnsupportedType = errors.New("unsupported type")
8+
)

prop/prop.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,57 @@ const (
5959
Interest = "INTEREST"
6060
OrgDirectory = "ORG-DIRECTORY"
6161
)
62+
63+
var (
64+
Props = map[string]string{
65+
Adr: "ADR",
66+
Anniversary: "ANNIVERSARY",
67+
BDay: "BDAY",
68+
CalAdrURI: "CALADRURI",
69+
CalURI: "CALURI",
70+
Categories: "CATEGORIES",
71+
Class: "CLASS",
72+
ClientPIDMap: "CLIENTPIDMAP",
73+
Email: "EMAIL",
74+
FbURL: "FBURL",
75+
Fn: "FN",
76+
Gender: "GENDER",
77+
Geo: "GEO",
78+
Key: "KEY",
79+
Kind: "KIND",
80+
Lang: "LANG",
81+
Logo: "LOGO",
82+
Mailer: "MAILER",
83+
Member: "MEMBER",
84+
N: "N",
85+
Name: "NAME",
86+
Nickname: "NICKNAME",
87+
Note: "NOTE",
88+
Org: "ORG",
89+
Photo: "PHOTO",
90+
ProdID: "PRODID",
91+
Profile: "PROFILE",
92+
Related: "RELATED",
93+
Rev: "REV",
94+
Role: "ROLE",
95+
Sound: "SOUND",
96+
Source: "SOURCE",
97+
Tel: "TEL",
98+
Title: "TITLE",
99+
Tz: "TZ",
100+
UID: "UID",
101+
URL: "URL",
102+
Version: "VERSION",
103+
XML: "XML",
104+
105+
// Additional properties
106+
BirthPlace: "BIRTHPLACE",
107+
DeathPlace: "DEATHPLACE",
108+
DeathDate: "DEATHDATE",
109+
Expertise: "EXPERTISE",
110+
Hobby: "HOBBY",
111+
IMPP: "IMPP",
112+
Interest: "INTEREST",
113+
OrgDirectory: "ORG-DIRECTORY",
114+
}
115+
)

vcard.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package vcard
33

44
import (
55
"bufio"
6+
"bytes"
7+
"fmt"
68
"io"
79
"os"
810
"reflect"
@@ -14,6 +16,9 @@ import (
1416
const (
1517
// VCardTagName represents the tag name used inside the struct VCard.
1618
VCardTagName = "vcard"
19+
20+
// VCardVersion represents the supported version of vCard.
21+
VCardVersion = "2.1"
1722
)
1823

1924
// VCard represents a single vCard with its fields.
@@ -112,6 +117,48 @@ func GetVCardsByReader(r io.Reader) ([]VCard, error) {
112117
return vcList, nil
113118
}
114119

120+
// Marshal returns the vCard encoding of v.
121+
func Marshal(v interface{}) ([]byte, error) {
122+
switch reflect.TypeOf(v).Kind() {
123+
case reflect.Struct:
124+
return marshalStruct(v)
125+
}
126+
127+
return nil, ErrUnsupportedType
128+
}
129+
130+
func marshalStruct(v interface{}) ([]byte, error) {
131+
elem := reflect.ValueOf(v)
132+
buffer := new(bytes.Buffer)
133+
134+
buffer.WriteString(prop.Begin + "\n")
135+
136+
buffer.WriteString(prop.Version)
137+
buffer.WriteString(":")
138+
buffer.WriteString(VCardVersion + "\n")
139+
140+
for i := 0; i < elem.NumField(); i++ {
141+
f := elem.Field(i)
142+
if f.Kind() != reflect.String {
143+
continue // No non string field is supported yet
144+
}
145+
146+
sf := elem.Type().Field(i)
147+
tag := sf.Tag.Get(VCardTagName)
148+
if tag != "" && f.CanInterface() {
149+
if property, ok := prop.Props[tag]; ok {
150+
buffer.WriteString(property)
151+
buffer.WriteString(":")
152+
buffer.WriteString(fmt.Sprintf("%s\n", f.Interface()))
153+
}
154+
}
155+
}
156+
157+
buffer.WriteString(prop.End + "\n")
158+
159+
return buffer.Bytes(), nil
160+
}
161+
115162
func readVCFProperty(vc *VCard, line string) {
116163
if line == prop.Begin || line == prop.End || len(strings.Trim(line, " \t")) == 0 {
117164
return

vcard_test.go

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
package vcard
1+
package vcard_test
22

33
import (
44
"bytes"
55
"testing"
66

7+
"github.com/mapaiva/vcard-go"
78
"github.com/stretchr/testify/assert"
89
)
910

10-
func TestVCardsByReader(t *testing.T) {
11+
func TestGetVCardsByReader(t *testing.T) {
1112
assert := assert.New(t)
1213

1314
t.Run("Test if it returns the expected fields given a valid reader", func(t *testing.T) {
14-
r := bytes.NewBuffer([]byte(`
15-
BEGIN:VCARD
15+
r := bytes.NewBuffer([]byte(`BEGIN:VCARD
1616
VERSION:2.1
1717
N:Prefect;Ford;
1818
FN:Ford Prefect
@@ -26,7 +26,7 @@ EMAIL;PREF:[email protected]
2626
END:VCARD
2727
`))
2828

29-
cards, err := GetVCardsByReader(r)
29+
cards, err := vcard.GetVCardsByReader(r)
3030

3131
assert.Nil(err)
3232
assert.NotNil(cards)
@@ -39,3 +39,44 @@ END:VCARD
3939
assert.Equal(cards[1].Email, "[email protected]")
4040
})
4141
}
42+
43+
func TestMarshal(t *testing.T) {
44+
assert := assert.New(t)
45+
46+
type vCard struct {
47+
StructuredName string `vcard:"N"`
48+
FormattedName string `vcard:"FN"`
49+
Email string `vcard:"EMAIL"`
50+
}
51+
52+
t.Run("Should marshal a struct into a vcard record encoding", func(t *testing.T) {
53+
expected := []byte(`BEGIN:VCARD
54+
VERSION:2.1
55+
N:Prefect;Ford;
56+
FN:Ford Prefect
57+
58+
END:VCARD
59+
`)
60+
card, err := vcard.Marshal(vCard{
61+
StructuredName: "Prefect;Ford;",
62+
FormattedName: "Ford Prefect",
63+
64+
})
65+
66+
assert.Nil(err)
67+
assert.Equal(string(expected), string(card))
68+
})
69+
70+
t.Run("Should return error trying to marshal a slice", func(t *testing.T) {
71+
_, err := vcard.Marshal([]vCard{
72+
{
73+
74+
StructuredName: "Prefect;Ford;",
75+
FormattedName: "Ford Prefect",
76+
77+
},
78+
})
79+
80+
assert.Equal(vcard.ErrUnsupportedType, err)
81+
})
82+
}

0 commit comments

Comments
 (0)