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

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

View File

@ -1,6 +1,6 @@
# Package # Package
version = "0.1.0" version = "0.1.3"
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"