7 Commits
0.1.0 ... 0.1.1

Author SHA1 Message Date
a1dac6eaaf Bump version. 2023-04-15 07:09:55 -05:00
2c625349bf README: add simple usage. 2023-04-15 07:09:03 -05:00
0f353e97ca Add test case for email preference. 2023-04-15 07:08:30 -05:00
c4717b4b00 Date types default to date-time as specified.
- `parseDateOrDateTime attempts to parse times starting from the most
  specific (date and time) to least specific.
- `set` and `add` functions allow adding multiple content items at once
  using varargs.
2023-04-15 07:03:24 -05:00
419d794c68 Added RFC 6352 (CardDav). 2023-04-15 06:58:57 -05:00
a0cd676521 Fix defect found testing EMAIL content types. 2023-04-10 16:10:49 -05:00
2a48974f3a Add basic description in the README. 2023-04-05 09:42:16 -05:00
7 changed files with 2823 additions and 31 deletions

View File

@ -1,3 +1,56 @@
# VCard
`nim-vcard` is a pure nim implementation of the VCard format defined in RFCs
2425, 2426, and 6350. It allows you to parse and serialize VCards, as well as
create VCards programmatically. It aims to be a complete implememtation,
supporting all of the features of the VCard3 standard. Because the standard
provides many features that may be rarely used, this library also provides a
simplified API for more typical use-cases.
## Example Usage
```vcard
BEGIN:VCARD
VERSION:3.0
UID: 5db6f100-e2d6-4e8d-951f-d920586bc069
N:Foster;Jack;Allen;;
FN:Allen Foster
REV:20230408T122102Z
EMAIL;TYPE=home;TYPE=pref:allen@fosters.test
EMAIL;TYPE=work:jack.foster@company.test
TEL;TYPE=CELL:+1 (555) 123-4567
END:VCARD
```
```nim
import vcard
# Reading in an existing vcard
let vcards = parseVCard3File("jack.vcf")
assert vcards.len == 1
let vcAllen = vcards[0]
assert vcAllen.email.len == 2
assert vcAllen.email[0].value == "allen@fosters.test"
assert vcAllen.n.first == "Jack"
# Creating a new VCard
var vcSusan: VCard3
vcSusan.add(
newVC3_N(given = "Susan", family = "Foster"),
newVC3_Email(value = "susan@fosters.test", emailType = @["PREF", $etInternet),
newVC3_Tel(
value = "+1 (555) 444-3889",
telType = @[$ttHome, $ttCell, $ttVoice, $ttMsg])
)
writeFile("susan.vcf", $vcSusan)
```
## Future Goals
* VCard 4.0 support
## Debugging ## Debugging
*Need to clean up and organize* *Need to clean up and organize*

2691
doc/rfc6352.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@ -12,16 +12,16 @@ const DATE_TIME_FMTS = [
"yyyy-MM-dd'T'HH:mm:ss'.'fffzzz", "yyyy-MM-dd'T'HH:mm:ss'.'fffzzz",
] ]
const ALL_FMTS = DATE_FMTS.toSeq & DATE_TIME_FMTS.toSeq const ALL_FMTS = DATE_TIME_FMTS.toSeq & DATE_FMTS.toSeq
proc parseDateTimeStr( proc parseDateTimeStr(
dateStr: string, dateStr: string,
dateFmts: openarray[string] dateFmts: openarray[string]
): DateTime {.inline.} = ): DateTime {.inline, raises:[ValueError].} =
for fmt in dateFmts: for fmt in dateFmts:
try: result = parse(dateStr, fmt) try: result = parse(dateStr, fmt)
except: discard except ValueError: discard
if not result.isInitialized: if not result.isInitialized:
raise newException(ValueError, "cannot parse date: " & dateStr ) raise newException(ValueError, "cannot parse date: " & dateStr )

View File

@ -388,7 +388,7 @@ func newVC3_Email*(
emailType = @[$etInternet], emailType = @[$etInternet],
group = none[string]()): VC3_Email = group = none[string]()): VC3_Email =
return VC3_Email(name: "EMAIL", emailType: emailType, group: group) return assignFields(VC3_Email(name: "EMAIL"), value, emailType, group)
func newVC3_Mailer*( func newVC3_Mailer*(
value: string, value: string,
@ -523,8 +523,8 @@ func newVC3_Sound*(
func newVC3_UID*(value: string, group = none[string]()): VC3_UID = func newVC3_UID*(value: string, group = none[string]()): VC3_UID =
return VC3_UID(name: "UID", value: value, group: group) return VC3_UID(name: "UID", value: value, group: group)
func newVC3_URL*(value: string, group = none[string]()): VC3_Url = func newVC3_URL*(value: string, group = none[string]()): VC3_URL =
return VC3_Url(name: "URL", value: value, group: group) return VC3_URL(name: "URL", value: value, group: group)
func newVC3_Version*(group = none[string]()): VC3_Version = func newVC3_Version*(group = none[string]()): VC3_Version =
return VC3_Version(name: "VERSION", value: "3.0", group: group) return VC3_Version(name: "VERSION", value: "3.0", group: group)
@ -551,7 +551,7 @@ func newVC3_XType*(
xParams: seq[VC3_XParam] = @[], xParams: seq[VC3_XParam] = @[],
group = none[string]()): VC3_XType = group = none[string]()): VC3_XType =
if not name.startsWith("x-"): if not name.startsWith("X-"):
raise newException(ValueError, "Extended types must begin with 'x-'.") raise newException(ValueError, "Extended types must begin with 'x-'.")
return assignFields( return assignFields(
@ -652,11 +652,11 @@ func sortstring*(vc3: VCard3): Option[VC3_SortString] = vc3.content.sortstring
func sound*(c: VC3_ContentList): seq[VC3_Sound] = findAll[VC3_Sound](c) func sound*(c: VC3_ContentList): seq[VC3_Sound] = findAll[VC3_Sound](c)
func sound*(vc3: VCard3): seq[VC3_Sound] = vc3.content.sound func sound*(vc3: VCard3): seq[VC3_Sound] = vc3.content.sound
func uid*(c: VC3_ContentList): Option[VC3_Uid] = findFirst[VC3_Uid](c) func uid*(c: VC3_ContentList): Option[VC3_UID] = findFirst[VC3_UID](c)
func uid*(vc3: VCard3): Option[VC3_Uid] = vc3.content.uid func uid*(vc3: VCard3): Option[VC3_UID] = vc3.content.uid
func url*(c: VC3_ContentList): Option[VC3_Url] = findFirst[VC3_Url](c) func url*(c: VC3_ContentList): Option[VC3_URL] = findFirst[VC3_URL](c)
func url*(vc3: VCard3): Option[VC3_Url] = vc3.content.url func url*(vc3: VCard3): Option[VC3_URL] = vc3.content.url
func version*(c: VC3_ContentList): VC3_Version = func version*(c: VC3_ContentList): VC3_Version =
let found = findFirst[VC3_Version](c) let found = findFirst[VC3_Version](c)
@ -680,26 +680,30 @@ func xTypes*(vc3: VCard3): seq[VC3_XType] = vc3.content.xTypes
# Setters # Setters
# ============================================================================= # =============================================================================
func set*[T](vc3: var VCard3, newContent: var T): void = func set*[T](vc3: var VCard3, content: varargs[T]): void =
for c in content:
var nc = c
let existingIdx = vc3.content.indexOfIt(it of T) let existingIdx = vc3.content.indexOfIt(it of T)
if existingIdx < 0: if existingIdx < 0:
newContent.contentId = vc3.takeContentId nc.contentId = vc3.takeContentId
vc3.content.add(newContent) vc3.content.add(nc)
else: else:
newContent.contentId = vc3.content[existingIdx].contentId nc.contentId = vc3.content[existingIdx].contentId
vc3.content[existingIdx] = newContent vc3.content[existingIdx] = nc
func set*[T](vc3: VCard3, newContent: var T): VCard3 = func set*[T](vc3: VCard3, content: varargs[T]): VCard3 =
result = vc3 result = vc3
result.set(newContent) result.set(content)
func add*[T](vc3: var VCard3, newContent: T): void = func add*[T](vc3: var VCard3, content: varargs[T]): void =
newContent.contentId = vc3.takeContentId for c in content:
vc3.content.add(newContent) var nc = c
nc.contentId = vc3.takeContentId
vc3.content.add(nc)
func add*[T](vc3: VCard3, newContent: T): VCard3 = func add*[T](vc3: VCard3, content: varargs[T]): VCard3 =
result = vc3 result = vc3
result.add(newContent) result.add(content)
func updateOrAdd*[T](vc3: var VCard3, content: seq[T]): VCard3 = func updateOrAdd*[T](vc3: var VCard3, content: seq[T]): VCard3 =
for c in content: for c in content:
@ -707,6 +711,13 @@ func updateOrAdd*[T](vc3: var VCard3, content: seq[T]): VCard3 =
if existingIdx < 0: vc3.content.add(c) if existingIdx < 0: vc3.content.add(c)
else: c.content[existingIdx] = c else: c.content[existingIdx] = c
func updateOrAdd*[T](vc3: VCard3, content: seq[T]): VCard3 =
result = vc3
for c in content:
let existingIdx = result.content.indexOfIt(it.contentId == c.contentId)
if existingIdx < 0: result.content.add(c)
else: c.content[existingIdx] = c
# TODO: simplify with macros? # TODO: simplify with macros?
# macro generateImmutableVersion()... # macro generateImmutableVersion()...
# generateImmutableVersion("set", "add", "setName", "addSource") # generateImmutableVersion("set", "add", "setName", "addSource")
@ -1294,8 +1305,10 @@ proc serialize(r: VC3_Rev): string =
result = r.nameWithGroup result = r.nameWithGroup
if r.valueType.isSome and r.valueType.get == "date-time": if r.valueType.isSome and r.valueType.get == "date-time":
result &= ";VALUE=date-time:" & r.value.format(DATETIME_FMT) result &= ";VALUE=date-time:" & r.value.format(DATETIME_FMT)
elif r.valueType.isSome and r.valueType.get == "date":
result &= ";VALUE=date-time:" & r.value.format(DATETIME_FMT)
else: else:
result &= ";VALUE=date:" & r.value.format(DATE_FMT) result &= r.value.format(DATETIME_FMT)
proc serialize(u: VC3_UID | VC3_URL | VC3_VERSION | VC3_Class): string = proc serialize(u: VC3_UID | VC3_URL | VC3_VERSION | VC3_Class): string =
result = u.nameWithGroup & ":" & u.value result = u.nameWithGroup & ":" & u.value
@ -1779,10 +1792,10 @@ proc parseContentLines(p: var VC3Parser): seq[VC3_Content] =
isInline = params.existsWithValue("ENCODING", "B"))) isInline = params.existsWithValue("ENCODING", "B")))
of $cnUid: of $cnUid:
result.add(newVC3_Uid(group = group, value = p.readValue)) result.add(newVC3_UID(group = group, value = p.readValue))
of $cnUrl: of $cnUrl:
result.add(newVC3_Url(group = group, value = p.readValue)) result.add(newVC3_URL(group = group, value = p.readValue))
of $cnVersion: of $cnVersion:
p.expect("3.0") p.expect("3.0")
@ -1801,7 +1814,7 @@ proc parseContentLines(p: var VC3Parser): seq[VC3_Content] =
isInline = params.existsWithValue("ENCODING", "B"))) isInline = params.existsWithValue("ENCODING", "B")))
else: else:
if not name.startsWith("x-"): if not name.startsWith("X-"):
p.error("unrecognized content type: '$1'" % [name]) p.error("unrecognized content type: '$1'" % [name])
result.add(newVC3_XType( result.add(newVC3_XType(

26
tests/jdb.vcf Executable file
View File

@ -0,0 +1,26 @@
BEGIN:VCARD
PRODID:-//CyrusIMAP.org//Cyrus 3.7.0-alpha0-927-gf4c98c8499-fm-202208..//EN
VERSION:3.0
UID:cdaf67dc-b702-41ac-9c26-bb61df3032d2
N:Bernard;Jonathan;;;
FN:Jonathan Bernard
ORG:;
TITLE:
NICKNAME:
NOTE:
REV:20220908T122102Z
EMAIL;TYPE=home;TYPE=pref:jonathan@jdbernard.com
EMAIL;TYPE=work:jdb@jdb-software.com
email2.X-ABLabel:Obsolete
email2.EMAIL:jonathan.bernard@sailpoint.com
email3.X-ABLabel:Obsolete
email3.EMAIL:jonathan.bernard@accenture.com
email4.X-ABLabel:Obsolete
email4.EMAIL:jbernard@fairwaytech.com
email5.X-ABLabel:Obsolete
email5.EMAIL:jobernar@linkedin.com
EMAIL;TYPE=work:jbernard@vectra.ai
TEL;TYPE=CELL:(512) 777-1602
tel1.X-ABLabel:Mobile (alernate)
tel1.TEL:(512) 784-2388
END:VCARD

View File

@ -53,3 +53,12 @@ suite "vcard/vcard3":
vcards[0].fn.value == "Frank Dawson" vcards[0].fn.value == "Frank Dawson"
vcards[0].email.len == 2 vcards[0].email.len == 2
(vcards[0].email --> find(it.emailType.contains("PREF"))).isSome (vcards[0].email --> find(it.emailType.contains("PREF"))).isSome
test "Jonathan Bernard VCard":
#const jdbVcard = readFile("tests/jdb.vcf")
let jdb = parseVCard3File("tests/jdb.vcf")[0]
check:
jdb.email.len == 7
jdb.email[0].value == "jonathan@jdbernard.com"
jdb.email[0].emailType.contains("pref")
jdb.fn.value == "Jonathan Bernard"

View File

@ -1,6 +1,6 @@
# Package # Package
version = "0.1.0" version = "0.1.1"
author = "Jonathan Bernard" author = "Jonathan Bernard"
description = "Nim parser for the vCard format version 3.0 (4.0 planned)." description = "Nim parser for the vCard format version 3.0 (4.0 planned)."
license = "MIT" license = "MIT"