10 Commits
0.1.0 ... 0.1.3

Author SHA1 Message Date
68554920e5 Fix bug in parsing TEL content. Rework unit tests.
- newVC3_Tel was not assigning the value provided to the constructed
  object.
- Private unit tests were run every time the code was compiled due to
  how the unittest library works. These now only run as part of the unit
  tests with `nimble test`.
2023-04-16 03:34:14 -05:00
7b71cb2dfe Extract example from the README to a runnable location. 2023-04-16 03:31:37 -05:00
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
12 changed files with 3039 additions and 219 deletions

View File

@ -1,10 +1,40 @@
# 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
```
https://github.com/jdbernard/nim-vcard/blob/4839ff64a8e6da1ad4803adbd71c0a53cae81c4e/examples/simple.nim#L1-L22
## Future Goals
* VCard 4.0 support
## Debugging
*Need to clean up and organize*
Run `tlexer` tests in gdb:
Run `tvcard3` tests in gdb:
```sh
$ cd tests
$ nim --debuginfo --linedir:on c tlexer
$ gdb --tui tlexer
$ nim --debuginfo --linedir:on c tvcard3
$ gdb --tui tvcard3

2691
doc/rfc6352.txt Normal file

File diff suppressed because it is too large Load Diff

10
examples/jack.vcf Normal file
View File

@ -0,0 +1,10 @@
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

22
examples/simple.nim Normal file
View File

@ -0,0 +1,22 @@
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.given[0] == "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)

3
src/vcard.nim Normal file
View File

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

View File

@ -180,11 +180,6 @@ proc getColNumber*(vcl: VCardLexer, pos: int): int =
if vcl.lineStart < pos: return pos - vcl.lineStart
else: return (vcl.buffer.len - vcl.lineStart) + pos
## Unit Tests
## ============================================================================
import std/unittest
proc dumpLexerState*(l: VCardLexer): string =
result =
"pos = " & $l.pos & "\p" &
@ -195,7 +190,9 @@ proc dumpLexerState*(l: VCardLexer): string =
"bufEnd = " & $l.bufEnd & "\p" &
"buffer = " & l.buffer & "\p"
suite "vcard/lexer":
## Unit Tests
## ============================================================================
proc runVcardLexerPrivateTests*() =
const longTestString =
"This is my test string. There are many like it but this one is mine."
@ -212,36 +209,34 @@ suite "vcard/lexer":
return false
return true
#test "fillBuffer doesn't double the buffer needlessly":
# var l: VCardLexer
proc readExpected(vcl: var VCardLexer, s: string): bool =
for i in 0..<s.len:
if vcl.read != s[i]:
return false
return true
test "can open and fill buffer":
# "can open and fill buffer":
block:
var l: VCardLexer
l.open(newStringStream("test"))
check:
l.bufferIs("test")
not l.isFull
l.readExpected("test")
assert l.bufferIs("test")
assert not l.isFull
assert l.readExpected("test")
test "refills buffer when emptied":
# "refills buffer when emptied":
block:
var l: VCardLexer
l.open(newStringStream("test"), 3)
check:
l.bufferIs("te")
l.isFull
l.read == 't'
l.read == 'e'
l.read == 's'
l.bufferIs("st")
l.read == 't'
assert l.bufferIs("te")
assert l.isFull
assert l.read == 't'
assert l.read == 'e'
assert l.read == 's'
assert l.bufferIs("st")
assert l.read == 't'
test "isFull correctness":
# "isFull correctness":
block:
var l = VCardLexer(
pos: 0,
bookmark: -1,
@ -251,104 +246,102 @@ suite "vcard/lexer":
# s e
# 0 1 2 3 4 5 6 7 8 9
check l.isFull
assert l.isFull
# s p e
# 0 1 2 3 4 5 6 7 8 9
discard l.read
check not l.isFull
assert not l.isFull
# e s
# 0 1 2 3 4 5 6 7 8 9
l.bufStart = 3
l.pos = 3
l.bufEnd = 2
check l.isFull
assert l.isFull
# e s p
# 0 1 2 3 4 5 6 7 8 9
discard l.read
check:
l.pos == 4
not l.isFull
assert l.pos == 4
assert not l.isFull
# e s
# 0 1 2 3 4 5 6 7 8 9
l.bufStart = 9
l.pos = 9
l.bufEnd = 8
check l.isFull
assert l.isFull
# p e s
# 0 1 2 3 4 5 6 7 8 9
discard l.read
check:
l.pos == 0
not l.isFull
assert l.pos == 0
assert not l.isFull
test "handles wrapped lines":
# "handles wrapped lines":
block:
var l: VCardLexer
l.open(newStringStream("line\r\n wrap\r\nline 2"), 3)
check l.readExpected("line wrap\r\nline 2")
assert l.readExpected("line wrap\r\nline 2")
test "fillBuffer correctness":
# "fillBuffer correctness":
block:
var l: VCardLexer
l.open(newStringStream(longTestString), 5)
check:
l.bufferIs(longTestString[0..<4])
l.isFull
l.bufStart == 0
l.bufEnd == 4
l.pos == 0
l.readExpected("Th")
not l.isFull
not l.atEnd
l.pos == 2
assert l.bufferIs(longTestString[0..<4])
assert l.isFull
assert l.bufStart == 0
assert l.bufEnd == 4
assert l.pos == 0
assert l.readExpected("Th")
assert not l.isFull
assert not l.atEnd
assert l.pos == 2
l.fillBuffer
check:
l.isFull
l.bufEnd == 1
l.pos == 2
l.bufStart == 2
assert l.isFull
assert l.bufEnd == 1
assert l.pos == 2
assert l.bufStart == 2
test "bookmark preserves the buffer":
# "bookmark preserves the buffer":
block:
var l: VCardLexer
l.open(newStringStream(longTestString), 7)
check:
l.buffer.len == 7
l.bufferIs(longTestString[0..<6])
l.isFull
l.bufEnd == 6
l.pos == 0
l.bookmark == -1
l.readExpected(longTestString[0..<5])
not l.isFull
not l.atEnd
l.pos == 5
assert l.buffer.len == 7
assert l.bufferIs(longTestString[0..<6])
assert l.isFull
assert l.bufEnd == 6
assert l.pos == 0
assert l.bookmark == -1
assert l.readExpected(longTestString[0..<5])
assert not l.isFull
assert not l.atEnd
assert l.pos == 5
l.setBookmark
# read enough to require us to refill the buffer.
check:
l.bookmark == 5
l.readExpected(longTestString[5..<10])
l.pos == 3
newStartIdx(l) == 5
l.buffer.len == 7
assert l.bookmark == 5
assert l.readExpected(longTestString[5..<10])
assert l.pos == 3
assert newStartIdx(l) == 5
assert l.buffer.len == 7
l.returnToBookmark
check:
l.bookmark == -1
l.pos == 5
assert l.bookmark == -1
assert l.pos == 5
test "readRune":
# "readRune":
block:
var l: VCardLexer
l.open(newStringStream("TEST"))
check:
l.bufferIs("TEST")
l.peekRune == Rune('T')
l.readRune == Rune('T')
l.readRune == Rune('E')
l.readRune == Rune('S')
l.readRune == Rune('T')
assert l.bufferIs("TEST")
assert l.peekRune == Rune('T')
assert l.readRune == Rune('T')
assert l.readRune == Rune('E')
assert l.readRune == Rune('S')
assert l.readRune == Rune('T')
when isMainModule: runVcardLexerTests()

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

@ -14,7 +14,7 @@ import std/[base64, macros, options, sequtils, streams, strutils, times,
import zero_functional
import vcard/private/[util, lexer]
import ./vcard/private/[util, lexer]
type
VC3_ValueTypes = enum
@ -381,14 +381,14 @@ func newVC3_Tel*(
telType = @[$ttVoice],
group = none[string]()): VC3_Tel =
return VC3_Tel(name: "TEL", telType: telType, group: group)
return assignFields(VC3_Tel(name: "TEL"), value, telType, group)
func newVC3_Email*(
value: string,
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
@ -1699,7 +1712,7 @@ proc parseContentLines(p: var VC3Parser): seq[VC3_Content] =
lat = parseFloat(partsStr[0]),
long = parseFloat(partsStr[1])
))
except:
except ValueError:
p.error("expected two float values separated by ';' for the GEO " &
"content type but received '" & rawValue & "'")
@ -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(
@ -1868,144 +1881,147 @@ stateDiagram-v2
## Private Function Unit Tests
## ============================================================================
import std/unittest
suite "vcard/vcard3/private":
proc runVcard3PrivateTests*() =
proc initParser(input: string): VC3Parser =
result = VC3Parser(filename: "private unittests")
lexer.open(result, newStringStream(input))
test "readGroup with group":
# "vcard/vcard3/private"
block:
var p = initParser("mygroup.BEGIN:VCARD")
let g = p.readGroup
assert g.isSome
assert g.get == "mygroup"
check:
g.isSome
g.get == "mygroup"
test "readGroup without group":
# "readGroup without group":
block:
var p = initParser("BEGIN:VCARD")
check p.readGroup.isNone
assert p.readGroup.isNone
test "expect (case-sensitive)":
# "expect (case-sensitive)":
block:
var p = initParser("BEGIN:VCARD")
p.expect("BEGIN", true)
try:
p.expect(":vcard", true)
check "" == "expect should have raised an error"
except: discard
assert "" == "expect should have raised an error"
except CatchableError: discard
test "expect (case-insensitive)":
# "expect (case-insensitive)":
block:
var p = initParser("BEGIN:VCARD")
p.expect("begin")
try:
p.expect("begin")
check "" == "expect should have raised an error"
except: discard
assert "" == "expect should have raised an error"
except CatchableError: discard
test "readName":
# "readName":
block:
var p = initParser("TEL;tel;x-Example;x-Are1+Name")
check:
p.readName == "TEL"
p.read == ';'
p.readName == "TEL"
p.read == ';'
p.readName == "X-EXAMPLE"
p.read == ';'
p.readName == "X-ARE1"
assert p.readName == "TEL"
assert p.read == ';'
assert p.readName == "TEL"
assert p.read == ';'
assert p.readName == "X-EXAMPLE"
assert p.read == ';'
assert p.readName == "X-ARE1"
try:
discard p.readName
check "" == "readName should have raised an error"
except: discard
assert "" == "readName should have raised an error"
except CatchableError: discard
test "readParamValue":
# "readParamValue":
block:
var p = initParser("TEL;TYPE=WORK;TYPE=Fun&Games%:+15551234567")
check:
p.readName == "TEL"
p.read == ';'
p.readName == "TYPE"
p.read == '='
p.readParamValue == "WORK"
p.read == ';'
p.readName == "TYPE"
p.read == '='
p.readParamValue == "Fun&Games%"
assert p.readName == "TEL"
assert p.read == ';'
assert p.readName == "TYPE"
assert p.read == '='
assert p.readParamValue == "WORK"
assert p.read == ';'
assert p.readName == "TYPE"
assert p.read == '='
assert p.readParamValue == "Fun&Games%"
test "readParams":
# "readParams":
block:
var p = initParser("TEL;TYPE=WORK;TYPE=Fun&Games%,Extra:+15551234567")
check p.readName == "TEL"
assert p.readName == "TEL"
let params = p.readParams
check:
params.len == 2
params[0].name == "TYPE"
params[0].values.len == 1
params[0].values[0] == "WORK"
params[1].name == "TYPE"
params[1].values.len == 2
params[1].values[0] == "Fun&Games%"
params[1].values[1] == "Extra"
assert params.len == 2
assert params[0].name == "TYPE"
assert params[0].values.len == 1
assert params[0].values[0] == "WORK"
assert params[1].name == "TYPE"
assert params[1].values.len == 2
assert params[1].values[0] == "Fun&Games%"
assert params[1].values[1] == "Extra"
test "readValue":
# "readValue":
block:
var p = initParser("TEL;TYPE=WORK:+15551234567\r\nFN:John Smith\r\n")
check p.skip("TEL")
assert p.skip("TEL")
discard p.readParams
check p.read == ':'
check p.readValue == "+15551234567"
assert p.read == ':'
assert p.readValue == "+15551234567"
p.expect("\r\n")
check p.readName == "FN"
assert p.readName == "FN"
discard p.readParams
check p.read == ':'
check p.readValue == "John Smith"
assert p.read == ':'
assert p.readValue == "John Smith"
test "readTextValueList":
# "readTextValueList":
block:
var p = initParser("Public;John;Quincey,Adams;Rev.;Esq:limited\r\n")
check:
p.readTextValueList == @["Public"]
p.readTextValueList(ifPrefix = some(';')) == @["John"]
p.readTextValueList(ifPrefix = some(';')) == @["Quincey", "Adams"]
p.readTextValueList(ifPrefix = some(';')) == @["Rev."]
p.readTextValueList(ifPrefix = some(';')) == @["Esq:limited"]
p.readTextValueList(ifPrefix = some(';')) == newSeq[string]()
assert p.readTextValueList == @["Public"]
assert p.readTextValueList(ifPrefix = some(';')) == @["John"]
assert p.readTextValueList(ifPrefix = some(';')) == @["Quincey", "Adams"]
assert p.readTextValueList(ifPrefix = some(';')) == @["Rev."]
assert p.readTextValueList(ifPrefix = some(';')) == @["Esq:limited"]
assert p.readTextValueList(ifPrefix = some(';')) == newSeq[string]()
test "existsWithValue":
# "existsWithValue":
block:
var p = initParser(";TYPE=WORK;TYPE=VOICE;TYPE=CELL")
let params = p.readParams
check:
params.existsWithValue("TYPE", "WORK")
params.existsWithValue("TYPE", "CELL")
not params.existsWithValue("TYPE", "ISDN")
assert params.existsWithValue("TYPE", "WORK")
assert params.existsWithValue("TYPE", "CELL")
assert not params.existsWithValue("TYPE", "ISDN")
test "getSingleValue":
# "getSingleValue":
block:
var p = initParser(";TYPE=WORK;TYPE=VOICE;TYPE=CELL")
let params = p.readParams
let val = params.getSingleValue("TYPE")
check:
val.isSome
val.get == "WORK"
params.getSingleValue("VALUE").isNone
assert val.isSome
assert val.get == "WORK"
assert params.getSingleValue("VALUE").isNone
test "getMultipleValues":
# "getMultipleValues":
block:
var p = initParser(";TYPE=WORK;TYPE=VOICE;TYPE=CELL")
let params = p.readParams
check:
params.getMultipleValues("TYPE") == @["WORK", "VOICE", "CELL"]
params.getMultipleValues("VALUE") == newSeq[string]()
assert params.getMultipleValues("TYPE") == @["WORK", "VOICE", "CELL"]
assert params.getMultipleValues("VALUE") == newSeq[string]()
test "validateNoParameters":
# "validateNoParameters":
block:
var p = initParser(";TYPE=WORK;TYPE=VOICE;TYPE=CELL")
let params = p.readParams
p.validateNoParameters(@[], "TEST")
try:
p.validateNoParameters(params, "TEST")
check "" == "validateNoParameters should have errored"
except: discard
assert "" == "validateNoParameters should have errored"
except CatchableError: discard
test "validateRequredParameters":
# "validateRequredParameters":
block:
var p = initParser(";CONTEXT=word;VALUE=uri;TYPE=CELL")
let params = p.readParams
p.validateRequiredParameters(params,
@ -2013,5 +2029,7 @@ suite "vcard/vcard3/private":
try:
p.validateRequiredParameters(params, [("TYPE", "VOICE")])
check "" == "validateRequiredParameters should have errored"
except: discard
assert "" == "validateRequiredParameters should have errored"
except CatchableError: discard
when isMainModule: runVcard3PrivateTests()

26
tests/jdb.vcf Executable file
View File

@ -0,0 +1,26 @@
BEGIN:VCARD
VERSION:3.0
PRODID:-//CyrusIMAP.org//Cyrus 3.7.0-alpha0-927-gf4c98c8499-fm-202208..//EN
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

@ -1,2 +1,6 @@
import unittest
import vcard/private/lexer
import ./vcard/private/lexer
suite "vcard/private/lexer":
test "private lexer tests":
runVcardLexerPrivateTests()

View File

@ -1,24 +1,47 @@
import options, unittest, vcard3, zero_functional
import options, unittest, zero_functional
import ./vcard
suite "vcard/vcard3":
let testVCard =
"BEGIN:VCARD\r\n" &
"VERSION:3.0\r\n" &
"FN:Mr. John Q. Public\\, Esq.\r\n" &
"N:Public;John;Quinlan;Mr.;Esq.\r\n" &
"END:VCARD\r\n"
test "vcard3/private tests":
runVcard3PrivateTests()
test "minimal VCard":
let vc = parseVCard3(testVCard)[0]
let jdbVCard = readFile("tests/jdb.vcf")
let jdb = parseVCard3(jdbVCard)[0]
test "parseVCard3":
check:
vc.n.family[0] == "Public"
vc.n.given[0] == "John"
vc.fn.value == "Mr. John Q. Public\\, Esq."
jdb.n.family == @["Bernard"]
jdb.n.given == @["Jonathan"]
jdb.fn.value == "Jonathan Bernard"
test "serialize minimal VCard":
let vc = parseVCard3(testVCard)[0]
check $vc == testVCard
test "parseVCard3File":
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"
test "email is parsed correctly":
check:
jdb.email.len == 7
jdb.email[0].value == "jonathan@jdbernard.com"
jdb.email[0].emailType.contains("pref")
jdb.email[0].emailType.contains("home")
jdb.email[1].value == "jdb@jdb-software.com"
jdb.email[1].emailType.contains("work")
jdb.email[2].group.isSome
jdb.email[2].group.get == "email2"
jdb.email[6].value == "jbernard@vectra.ai"
jdb.email[6].emailType.contains("work")
test "tel is parsed correctly":
check:
jdb.tel.len == 2
jdb.tel[0].value == "(512) 777-1602"
jdb.tel[0].telType.contains("CELL")
test "RFC2426 Author's VCards":
let vcardsStr =

View File

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