8 Commits
0.1.0 ... 0.1.2

Author SHA1 Message Date
47c62cce6d Restructure to follow standard nimble package format. 2023-04-15 07:40:58 -05:00
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
8 changed files with 2826 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
*Need to clean up and organize*

2691
doc/rfc6352.txt Normal file

File diff suppressed because it is too large Load Diff

3
src/vcard.nim Normal file
View File

@ -0,0 +1,3 @@
import vcard/vcard3
export vcard3

View File

@ -12,16 +12,16 @@ const DATE_TIME_FMTS = [
"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(
dateStr: string,
dateFmts: openarray[string]
): DateTime {.inline.} =
): DateTime {.inline, raises:[ValueError].} =
for fmt in dateFmts:
try: result = parse(dateStr, fmt)
except: discard
except ValueError: discard
if not result.isInitialized:
raise newException(ValueError, "cannot parse date: " & dateStr )

View File

@ -388,7 +388,7 @@ func newVC3_Email*(
emailType = @[$etInternet],
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*(
value: string,
@ -523,8 +523,8 @@ func newVC3_Sound*(
func newVC3_UID*(value: string, group = none[string]()): VC3_UID =
return VC3_UID(name: "UID", value: value, group: group)
func newVC3_URL*(value: string, group = none[string]()): VC3_Url =
return VC3_Url(name: "URL", value: value, group: group)
func newVC3_URL*(value: string, group = none[string]()): VC3_URL =
return VC3_URL(name: "URL", value: value, group: group)
func newVC3_Version*(group = none[string]()): VC3_Version =
return VC3_Version(name: "VERSION", value: "3.0", group: group)
@ -551,7 +551,7 @@ func newVC3_XType*(
xParams: seq[VC3_XParam] = @[],
group = none[string]()): VC3_XType =
if not name.startsWith("x-"):
if not name.startsWith("X-"):
raise newException(ValueError, "Extended types must begin with 'x-'.")
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*(vc3: VCard3): seq[VC3_Sound] = vc3.content.sound
func uid*(c: VC3_ContentList): Option[VC3_Uid] = findFirst[VC3_Uid](c)
func uid*(vc3: VCard3): Option[VC3_Uid] = vc3.content.uid
func uid*(c: VC3_ContentList): Option[VC3_UID] = findFirst[VC3_UID](c)
func uid*(vc3: VCard3): Option[VC3_UID] = vc3.content.uid
func url*(c: VC3_ContentList): Option[VC3_Url] = findFirst[VC3_Url](c)
func url*(vc3: VCard3): Option[VC3_Url] = vc3.content.url
func url*(c: VC3_ContentList): Option[VC3_URL] = findFirst[VC3_URL](c)
func url*(vc3: VCard3): Option[VC3_URL] = vc3.content.url
func version*(c: VC3_ContentList): VC3_Version =
let found = findFirst[VC3_Version](c)
@ -680,26 +680,30 @@ func xTypes*(vc3: VCard3): seq[VC3_XType] = vc3.content.xTypes
# Setters
# =============================================================================
func set*[T](vc3: var VCard3, newContent: var T): void =
let existingIdx = vc3.content.indexOfIt(it of T)
if existingIdx < 0:
newContent.contentId = vc3.takeContentId
vc3.content.add(newContent)
else:
newContent.contentId = vc3.content[existingIdx].contentId
vc3.content[existingIdx] = newContent
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)
if existingIdx < 0:
nc.contentId = vc3.takeContentId
vc3.content.add(nc)
else:
nc.contentId = vc3.content[existingIdx].contentId
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.set(newContent)
result.set(content)
func add*[T](vc3: var VCard3, newContent: T): void =
newContent.contentId = vc3.takeContentId
vc3.content.add(newContent)
func add*[T](vc3: var VCard3, content: varargs[T]): void =
for c in content:
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.add(newContent)
result.add(content)
func updateOrAdd*[T](vc3: var VCard3, content: seq[T]): VCard3 =
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)
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?
# macro generateImmutableVersion()...
# generateImmutableVersion("set", "add", "setName", "addSource")
@ -1294,8 +1305,10 @@ proc serialize(r: VC3_Rev): string =
result = r.nameWithGroup
if r.valueType.isSome and r.valueType.get == "date-time":
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:
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 =
result = u.nameWithGroup & ":" & u.value
@ -1779,10 +1792,10 @@ proc parseContentLines(p: var VC3Parser): seq[VC3_Content] =
isInline = params.existsWithValue("ENCODING", "B")))
of $cnUid:
result.add(newVC3_Uid(group = group, value = p.readValue))
result.add(newVC3_UID(group = group, value = p.readValue))
of $cnUrl:
result.add(newVC3_Url(group = group, value = p.readValue))
result.add(newVC3_URL(group = group, value = p.readValue))
of $cnVersion:
p.expect("3.0")
@ -1801,7 +1814,7 @@ proc parseContentLines(p: var VC3Parser): seq[VC3_Content] =
isInline = params.existsWithValue("ENCODING", "B")))
else:
if not name.startsWith("x-"):
if not name.startsWith("X-"):
p.error("unrecognized content type: '$1'" % [name])
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].email.len == 2
(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
version = "0.1.0"
version = "0.1.2"
author = "Jonathan Bernard"
description = "Nim parser for the vCard format version 3.0 (4.0 planned)."
license = "MIT"