Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
68554920e5 | |||
7b71cb2dfe | |||
47c62cce6d | |||
a1dac6eaaf | |||
2c625349bf | |||
0f353e97ca | |||
c4717b4b00 | |||
419d794c68 | |||
a0cd676521 | |||
2a48974f3a |
36
README.md
36
README.md
@ -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
2691
doc/rfc6352.txt
Normal file
File diff suppressed because it is too large
Load Diff
10
examples/jack.vcf
Normal file
10
examples/jack.vcf
Normal 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
22
examples/simple.nim
Normal 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
3
src/vcard.nim
Normal file
@ -0,0 +1,3 @@
|
||||
import vcard/vcard3
|
||||
|
||||
export vcard3
|
@ -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()
|
||||
|
@ -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 )
|
||||
|
@ -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
26
tests/jdb.vcf
Executable 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
|
@ -1,2 +1,6 @@
|
||||
import unittest
|
||||
import vcard/private/lexer
|
||||
import ./vcard/private/lexer
|
||||
|
||||
suite "vcard/private/lexer":
|
||||
test "private lexer tests":
|
||||
runVcardLexerPrivateTests()
|
||||
|
@ -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 =
|
||||
|
@ -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"
|
||||
|
Reference in New Issue
Block a user