vcard4: Complete implementation.

- Parsers and serializers are now present for all property types.
- Tests exist to cover parsing for most value types. Many property types
  share the same parsing logic based on their value type. We have
  created unit tests to cover each value type, not neccesarily all
  properties individually.
This commit is contained in:
Jonathan Bernard 2023-05-02 22:37:23 -05:00
parent daa58518e3
commit 98c300fee2
4 changed files with 831 additions and 198 deletions

View File

@ -1,18 +1,13 @@
import std/[genasts, macros, options, sets, streams, strutils, tables, times, unicode]
import std/[algorithm, genasts, macros, options, sequtils, sets, streams,
strutils, tables, times, unicode]
import zero_functional
from std/sequtils import toSeq
#from std/sequtils import toSeq
import ./private/[common, lexer, util]
type
VC4_Cardinality = enum
vccAtMostOne,
vccExactlyOne,
vccAtLeastOne
vccAny
VC4_ValueType = enum
VC4_ValueType* = enum
vtText = "text",
vtTextList = "text-list",
vtUri = "uri",
@ -74,43 +69,43 @@ type
## Non-standard, added to be a catch-all for non-standard names
pnUnknown = "UNKNOWN"
const propertyCardMap: Table[VC4_PropertyName, VC4_Cardinality] = [
(pnSource, vccAny),
(pnKind, vccAtMostOne),
(pnXml, vccAny),
(pnFn, vccAtLeastOne),
(pnN, vccAtMostOne),
(pnNickname, vccAny),
(pnPhoto, vccAny),
(pnBday, vccAtMostOne),
(pnAnniversary, vccAtMostOne),
(pnGender, vccAtMostOne),
(pnAdr, vccAny),
(pnTel, vccAny),
(pnEmail, vccAny),
(pnImpp, vccAny),
(pnLang, vccAny),
(pnTz, vccAny),
(pnGeo, vccAny),
(pnTitle, vccAny),
(pnRole, vccAny),
(pnLogo, vccAny),
(pnOrg, vccAny),
(pnMember, vccAny),
(pnRelated, vccAny),
(pnCategories, vccAny),
(pnNote, vccAny),
(pnProdId, vccAtMostOne),
(pnRev, vccAtMostOne),
(pnSound, vccAny),
(pnUid, vccAtMostOne),
(pnClientPidMap, vccAny),
(pnUrl, vccAny),
(pnVersion, vccExactlyOne),
(pnKey, vccAny),
(pnFbUrl, vccAny),
(pnCaladrUri, vccAny),
(pnCalUri, vccAny)
const propertyCardMap: Table[VC4_PropertyName, VC_PropCardinality] = [
(pnSource, vpcAny),
(pnKind, vpcAtMostOne),
(pnXml, vpcAny),
(pnFn, vpcAtLeastOne),
(pnN, vpcAtMostOne),
(pnNickname, vpcAny),
(pnPhoto, vpcAny),
(pnBday, vpcAtMostOne),
(pnAnniversary, vpcAtMostOne),
(pnGender, vpcAtMostOne),
(pnAdr, vpcAny),
(pnTel, vpcAny),
(pnEmail, vpcAny),
(pnImpp, vpcAny),
(pnLang, vpcAny),
(pnTz, vpcAny),
(pnGeo, vpcAny),
(pnTitle, vpcAny),
(pnRole, vpcAny),
(pnLogo, vpcAny),
(pnOrg, vpcAny),
(pnMember, vpcAny),
(pnRelated, vpcAny),
(pnCategories, vpcAny),
(pnNote, vpcAny),
(pnProdId, vpcAtMostOne),
(pnRev, vpcAtMostOne),
(pnSound, vpcAny),
(pnUid, vpcAtMostOne),
(pnClientPidMap, vpcAny),
(pnUrl, vpcAny),
(pnVersion, vpcExactlyOne),
(pnKey, vpcAny),
(pnFbUrl, vpcAny),
(pnCaladrUri, vpcAny),
(pnCalUri, vpcAny)
].toTable()
const fixedValueTypeProperties = [
@ -156,25 +151,26 @@ const fixedValueTypeProperties = [
(pnCalUri, vtUri)
]
const supportedParameters: Table[string, HashSet[VC4_PropertyName]] = [
const supportedParams: Table[string, HashSet[VC4_PropertyName]] = [
("LANGUAGE", [pnFn, pnN, pnNickname, pnBday, pnAdr, pnTitle, pnRole,
pnLogo, pnOrg, pnRelated, pnNote, pnSound].toHashSet),
("PREF", (propertyCardMap.pairs.toSeq -->
filter(it[1] == vccAtLeastOne or it[1] == vccAny).
filter(it[1] == vpcAtLeastOne or it[1] == vpcAny).
map(it[0])).
toHashSet),
# ("ALTID", all properties),
("PID", (propertyCardMap.pairs.toSeq -->
filter((it[1] == vccAtLeastOne or it[1] == vccAny) and
filter((it[1] == vpcAtLeastOne or it[1] == vpcAny) and
it[0] != pnClientPidMap).
map(it[0])).toHashSet),
("TYPE", @[ pnFn, pnNickname, pnPhoto, pnAdr, pnTel, pnEmail, pnImpp, pnLang,
pnTz, pnGeo, pnTitle, pnRole, pnLogo, pnOrg, pnRelated, pnCategories,
pnNote, pnSound, pnUrl, pnKey, pnFburl, pnCaladrUri, pnCalUri ].toHashSet),
].toTable
const TIMESTAMP_FORMATS = [
@ -184,8 +180,12 @@ const TIMESTAMP_FORMATS = [
"yyyyMMdd'T'hhmmss"
]
const TEXT_CHARS = WSP + NON_ASCII + { '\x21'..'\x2B', '\x2D'..'\x7E' }
const COMPONENT_CHARS = WSP + NON_ASCII +
{ '\x21'..'\x2B', '\x2D'..'\x3A', '\x3C'..'\x7E' }
macro genPropTypes(
props: static[openarray[ tuple[a: VC4_PropertyName, b: VC4_ValueType]]]
props: static[openarray[(VC4_PropertyName, VC4_ValueType)]]
): untyped =
result = newNimNode(nnkTypeSection)
@ -230,7 +230,6 @@ type
group*: Option[string]
params*: seq[VCParam]
# TODO: write accessors
VC4_DateTimeOrTextProperty* = ref object of VC4_Property
valueType: VC4_ValueType # should only be vtDateAndOrTime or vtText
value*: string
@ -259,7 +258,7 @@ type
VC4_DateTimeProperty* = ref object of VC4_Property
value*: DateTime
VC4_UnknownProperty* = ref object of VC4_Property
VC4_Unknown* = ref object of VC4_Property
name*: string
value*: string
@ -332,7 +331,7 @@ type
VC4_Gender* = ref object of VC4_Property
sex*: Option[VC4_Sex]
gender*: Option[string]
genderIdentity*: Option[string]
VC4_Adr* = ref object of VC4_Property
poBox*: string
@ -366,11 +365,9 @@ func flattenParameters(
): seq[VCParam] =
let paramTable = newTable[string, seq[string]]()
let allParams: seq[VCParam] =
params &
addtlParams.toSeq --> filter(it.values.len > 0)
let allParams = params & toSeq(addtlParams)
for p in allParams:
for p in (allParams --> filter(it.values.len > 0)):
let pname = p.name.toUpper
if paramTable.contains(pname):
for v in p.values:
@ -388,6 +385,7 @@ proc parseDateAndOrTime[T](
value: string
): void =
prop.value = value
var p = VCardParser(filename: value)
try:
@ -404,30 +402,32 @@ proc parseDateAndOrTime[T](
if p.peek != 'T':
# Attempt to parse the year
if p.peek == '-': p.expect("--")
else: prop.year = some(parseInt(p.read & p.read))
else: prop.year = some(parseInt(p.readLen(4)))
# Attempt to parse the month
if DIGIT.contains(p.peek) or p.peek == '-':
if p.peek == '-': p.expect("-")
else: prop.month = some(parseInt(p.read & p.read))
else: prop.month = some(parseInt(p.readLen(2)))
# Attempt to parse the month
if DIGIT.contains(p.peek):
prop.day = some(parseInt(p.read & p.read))
prop.day = some(parseInt(p.readLen(2)))
if p.peek == 'T':
p.expect("T")
# Attempt to parse the hour
if p.peek == '-': p.expect("-")
else: prop.hour = some(parseInt(p.read & p.read))
else: prop.hour = some(parseInt(p.readLen(2)))
# Attempt to parse the minute
if DIGIT.contains(p.peek) or p.peek == '-':
if p.peek == '-': p.expect("-")
else: prop.minute = some(parseInt(p.read & p.read))
else: prop.minute = some(parseInt(p.readLen(2)))
# Attempt to parse the second
if DIGIT.contains(p.peek):
prop.second = some(parseInt(p.read & p.read))
prop.second = some(parseInt(p.readLen(2)))
# Attempt to parse the timezone
if {'-', '+', 'Z'}.contains(p.peek):
@ -445,9 +445,33 @@ proc parseDateAndOrTime[T](
p.error("unable to parse date-and-or-time value: " & p.readSinceBookmark)
finally: p.unsetBookmark
proc parseTimestamp(value: string): DateTime =
for fmt in TIMESTAMP_FORMATS:
try: return value.parse(fmt)
except: discard
raise newException(VCardParsingError, "unable to parse timestamp value: " & value)
func parsePidValues(param: VCParam): seq[PidValue]
{.raises:[VCardParsingError].} =
result = @[]
for v in param.values:
try:
let pieces = v.split(".")
if pieces.len != 2: raise newException(ValueError, "")
result.add(PidValue(
propertyId: parseInt(pieces[0]),
sourceId: parseInt(pieces[1])))
except ValueError:
raise newException(VCardParsingError, "PID value expected to be two " &
"integers separated by '.' (2.1 for example)")
template validateType(p: VCardParser, params: seq[VCParam], t: VC4_ValueType) =
p.validateRequiredParameters(params, [("VALUE", $t)])
func cmp[T: VC4_Property](x, y: T): int =
return cmp(x.pref, y.pref)
# Initializers
# =============================================================================
@ -481,7 +505,7 @@ func addConditionalParams(prop: VC4_PropertyName, funcDef: NimNode) =
raise newException(ValueError, "cannot generate conditional params " &
"initialization code for this function shape:\n\r " & funcDef.treeRepr)
if supportedParameters["LANGUAGE"].contains(prop):
if supportedParams["LANGUAGE"].contains(prop):
# Add "language" as a function parameter
formalParams.add(newIdentDefs(
ident("language"),
@ -491,7 +515,7 @@ func addConditionalParams(prop: VC4_PropertyName, funcDef: NimNode) =
paramsInit.add(quote do:
("LANGUAGE", if language.isSome: @[language.get] else: @[]))
if supportedParameters["PREF"].contains(prop):
if supportedParams["PREF"].contains(prop):
# Add "pref" and "pids" as function parameters
formalParams.add(newIdentDefs(
ident("pref"),
@ -502,7 +526,7 @@ func addConditionalParams(prop: VC4_PropertyName, funcDef: NimNode) =
("PREF", if pref.isSome: @[$pref.get] else: @[]))
if supportedParameters["PID"].contains(prop):
if supportedParams["PID"].contains(prop):
# Add "pids" as a function parameter
formalParams.add(newIdentDefs(
ident("pids"),
@ -514,7 +538,7 @@ func addConditionalParams(prop: VC4_PropertyName, funcDef: NimNode) =
paramsInit.add(quote do: ("PID", pids --> map($it)))
if supportedParameters["TYPE"].contains(prop):
if supportedParams["TYPE"].contains(prop):
# Add "type" as a function parameter
formalParams.add(newIdentDefs(
ident("types"),
@ -526,21 +550,29 @@ func addConditionalParams(prop: VC4_PropertyName, funcDef: NimNode) =
paramsInit.add(quote do: ("TYPE", types))
func namesForProp(prop: VC4_PropertyName):
tuple[enumName, funcName, typeName: NimNode] =
tuple[enumName, typeName, initFuncName, accessorName: NimNode] =
var name: string = $prop
if name.len > 1: name = name[0] & name[1..^1].toLower
return (ident("pn" & name), ident("newVC4_" & name), ident("VC4_" & name))
return (
ident("pn" & name),
ident("VC4_" & name),
ident("newVC4_" & name),
ident(name.toLower))
macro genDateTimeOrTextPropInitializers(
properties: static[openarray[VC4_PropertyName]]
): untyped =
# TODO: the below does not provide for the case where you want to initialize
# a property with a date-and-or-time value that is not a specific DateTime
# instant (for example, a truncated date like "BDAY:--1224", a birthday on
# Dec. 24th without specifying the year.
result = newStmtList()
for prop in properties:
let (enumName, funcName, typeName) = namesForProp(prop)
let datetimeFuncDef = genAstOpt({kDirtyTemplate}, enumName, funcName, typeName):
func funcName*(
let (enumName, typeName, initFuncName, _) = namesForProp(prop)
let datetimeFuncDef = genAstOpt({kDirtyTemplate}, enumName, initFuncName, typeName):
func initFuncName*(
value: DateTime,
altId: Option[string] = none[string](),
group: Option[string] = none[string](),
@ -559,9 +591,10 @@ macro genDateTimeOrTextPropInitializers(
timezone: some(value.format("ZZZ")),
valueType: vtDateAndOrTime)
let textFuncDef = genAstOpt({kDirtyTemplate}, enumName, funcName, typeName):
proc funcName*(
let textFuncDef = genAstOpt({kDirtyTemplate}, enumName, initFuncName, typeName):
proc initFuncName*(
value: string,
valueType: Option[string] = some($vtDateAndOrTime),
altId: Option[string] = none[string](),
group: Option[string] = none[string](),
params: seq[VCParam] = @[]): typeName =
@ -572,6 +605,7 @@ macro genDateTimeOrTextPropInitializers(
value: value,
valueType: vtText)
if valueType.isNone or valueType.get == $vtDateAndOrTime:
result.parseDateAndOrTime(value)
addConditionalParams(prop, datetimeFuncDef)
@ -585,9 +619,9 @@ macro genTextPropInitializers(
result = newStmtList()
for prop in properties:
let (enumName, funcName, typeName) = namesForProp(prop)
let funcDef = genAstOpt({kDirtyTemplate}, enumName, funcName, typeName):
func funcName*(
let (enumName, typeName, initFuncName, _) = namesForProp(prop)
let funcDef = genAstOpt({kDirtyTemplate}, enumName, initFuncName, typeName):
func initFuncName*(
value: string,
altId: Option[string] = none[string](),
group: Option[string] = none[string](),
@ -608,9 +642,9 @@ macro genTextListPropInitializers(
result = newStmtList()
for prop in properties:
let (enumName, funcName, typeName) = namesForProp(prop)
let funcDef = genAstOpt({kDirtyTemplate}, enumName, funcName, typeName):
func funcName*(
let (enumName, typeName, initFuncName, _) = namesForProp(prop)
let funcDef = genAstOpt({kDirtyTemplate}, enumName, initFuncName, typeName):
func initFuncName*(
value: seq[string],
altId: Option[string] = none[string](),
group: Option[string] = none[string](),
@ -630,9 +664,9 @@ macro genTextOrUriPropInitializers(
result = newStmtList()
for prop in properties:
let (enumName, funcName, typeName) = namesForProp(prop)
let funcDef = genAstOpt({kDirtyTemplate}, enumName, funcName, typeName):
func funcName*(
let (enumName, typeName, initFuncName, _) = namesForProp(prop)
let funcDef = genAstOpt({kDirtyTemplate}, enumName, initFuncName, typeName):
func initFuncName*(
value: string,
isUrl = false,
altId: Option[string] = none[string](),
@ -653,9 +687,9 @@ macro genUriPropInitializers(
result = newStmtList()
for prop in properties:
let (enumName, funcName, typeName) = namesForProp(prop)
let funcDef = genAstOpt({kDirtyTemplate}, enumName, funcName, typeName):
func funcName*(
let (enumName, typeName, initFuncName, _) = namesForProp(prop)
let funcDef = genAstOpt({kDirtyTemplate}, enumName, initFuncName, typeName):
func initFuncName*(
value: string,
altId: Option[string] = none[string](),
mediaType: Option[string] = none[string](),
@ -703,7 +737,7 @@ func newVC4_N*(
func newVC4_Gender*(
sex: Option[VC4_Sex] = none[VC4_Sex](),
gender: Option[string] = none[string](),
genderIdentity: Option[string] = none[string](),
altId: Option[string] = none[string](),
group: Option[string] = none[string](),
params: seq[VCParam] = @[]): VC4_Gender =
@ -711,7 +745,7 @@ func newVC4_Gender*(
return assignFields(
VC4_Gender(params: flattenParameters(params,
("ALTID", if altId.isSome: @[altId.get] else: @[]))),
sex, gender, group)
sex, genderIdentity, group)
func newVC4_Adr*(
poBox = "",
@ -769,6 +803,118 @@ func newVC4_Rev*(
# Accessors
# =============================================================================
macro genPropAccessors(
properties: static[openarray[(VC4_PropertyName, VC_PropCardinality)]]
): untyped =
result = newStmtList()
for (pn, pCard) in properties:
let (_, typeName, _, funcName) = namesForProp(pn)
case pCard:
of vpcAtMostOne:
let funcDef = genAstOpt({kDirtyTemplate}, funcName, pn, typeName):
func funcName*(vc4: VCard4): Option[typeName] =
let alts = allAlternatives[typeName](vc4)
if alts.len > 1:
raise newException(ValueError,
("VCard should have at most one $# property, but $# " &
"distinct properties were found") % [$pn, $alts.len])
if alts.len == 0: result = none[typeName]()
else: result = some(alts[toSeq(alts.keys)[0]][0])
result.add(funcDef)
of vpcExactlyOne:
let funcDef = genAstOpt({kDirtyTemplate}, funcName, pn, typeName):
func funcName*(vc4: VCard4): typeName =
let alts = allAlternatives[typeName](vc4)
if alts.len != 1:
raise newException(ValueError,
"VCard should have exactly one $# property, but $# were found" %
[$pn, $alts.len])
result = alts[toSeq(alts.keys)[0]][0]
result.add(funcDef)
of vpcAtLeastOne, vpcAny:
let funcDef = genAstOpt({kDirtyTemplate}, funcName, typeName):
func funcName*(vc4: VCard4): seq[typeName] =
result = findAll[typeName](vc4.content)
result.add(funcDef)
macro genNameAccessors(propNames: static[seq[VC4_PropertyName]]): untyped =
result = genAstOpt({kDirtyTemplate}):
func name*(p: VC4_Property): string =
if p of VC4_Unknown:
return cast[VC4_Unknown](p).name
let genericIfBlock = result[6][0]
let memSafePNs = propNames
let propNamesToProcess = (memSafePNs --> filter(it != pnUnknown))
for pn in propNamesToProcess:
let (enumName, typeName, _, _) = namesForProp(pn)
let genericCond = nnkElifExpr.newTree(
nnkInfix.newTree(ident("of"), ident("p"), typeName),
quote do: return $`enumName`)
genericIfBlock.add(genericCond)
# echo result.repr
macro genLanguageAccessors(props: static[openarray[VC4_PropertyName]]): untyped =
result = newStmtList()
for p in props:
var name = $p
if name.len > 1: name = name[0] & name[1..^1].toLower
let typeName = ident("VC4_" & name)
let langFunc = genAstOpt({kDirtyTemplate}, typeName):
func language*(prop: typeName): Option[string] =
let langParam = prop.params --> find(it.name == "LANGUAGE")
if langParam.isSome and langParam.get.values.len > 0:
return some(langParam.get.values[0])
else: return none[string]()
result.add(langFunc)
macro genPrefAccessors(props: static[openarray[VC4_PropertyName]]): untyped =
result = newStmtList()
for p in props:
var name = $p
if name.len > 1: name = name[0] & name[1..^1].toLower
let typeName = ident("VC4_" & name)
let prefFunc = genAstOpt({kDirtyTemplate}, typeName):
func pref*(prop: typeName): int =
let prefParam = prop.params --> find(it.name == "PREF")
if prefParam.isSome and prefParam.get.values.len > 0:
return parseInt(prefParam.get.values[0])
else: return 101
result.add(prefFunc)
macro genPidAccessors(props: static[openarray[VC4_PropertyName]]): untyped =
result = newStmtList()
for p in props:
var name = $p
if name.len > 1: name = name[0] & name[1..^1].toLower
let typeName = ident("VC4_" & name)
let pidFunc = genAstOpt({kDirtyTemplate}, typeName):
func pid*(prop: typeName): seq[PidValue] =
let pidParam = prop.params --> find(it.name == "PREF")
if pidParam.isSome: return parsePidValues(pidParam.get)
else: return @[]
result.add(pidFunc)
macro genTypeAccessors(props: static[openarray[VC4_PropertyName]]): untyped =
result = newStmtList()
@ -785,44 +931,42 @@ macro genTypeAccessors(props: static[openarray[VC4_PropertyName]]): untyped =
else: return @[]
result.add(typeFun)
genTypeAccessors(supportedParameters["TYPE"].toSeq())
func inPrefOrder*[T: VC4_Property](props: seq[T]): seq[T] =
return props.sorted(vcard4.cmp[T])
macro genNameAccessors(propNames: static[seq[VC4_PropertyName]]): untyped =
result = newStmtList()
func altId*(p: VC4_Property): Option[string] =
p.params.getSingleValue("ALTID")
let genericFunc = genAstOpt({kDirtyTemplate}):
func name*(p: VC4_Property): string =
if p of VC4_UnknownProperty:
return cast[VC4_UnknownProperty](p).name
func valueType*(p: VC4_Property): Option[string] =
p.params.getSingleValue("VALUE")
let genericIfBlock = genericFunc[6][0]
result.add(genericFunc)
func allAlternatives*[T](vc4: VCard4): Table[string, seq[T]] =
result = initTable[string, seq[T]]()
block:
let funcDef = genAstOpt({kDirtyTemplate}):
func name*(p: VC4_UnknownProperty): string = p.name
for p in vc4.content:
if p of T:
let altId =
if p.altId.isSome: p.altId.get
else: ""
result.add(funcDef)
if not result.contains(altId): result[altId] = @[cast[T](p)]
else: result[altId].add(cast[T](p))
let memSafePNs = propNames
let propNamesToProcess = (memSafePNs --> filter(it != pnUnknown))
for pn in propNamesToProcess:
let (enumName, _, typeName) = namesForProp(pn)
let genericCond = nnkElifExpr.newTree(
nnkInfix.newTree(ident("of"), ident("p"), typeName),
quote do: return $`enumName`)
let funcDef = genAstOpt({kDirtyTemplate}, enumName, typeName):
func name*(p: typeName): string = $enumName
result.add(funcDef)
genericIfBlock.add(genericCond)
# echo result.repr
genPropAccessors(propertyCardMap.pairs.toSeq -->
filter(not [pnVersion, pnUnknown].contains(it[0])))
genNameAccessors(toSeq(VC4_PropertyName))
func customProp*(vc4: VCard4, name: string): seq[VC4_Unknown] =
result = vc4.content -->
filter(it of VC4_Unknown and it.name == name).
map(cast[VC4_Unknown](it))
genLanguageAccessors(supportedParams["LANGUAGE"].toSeq())
genPidAccessors(supportedParams["PID"].toSeq())
genPrefAccessors(supportedParams["PREF"].toSeq())
genTypeAccessors(supportedParams["TYPE"].toSeq())
# Setters
# =============================================================================
@ -851,50 +995,120 @@ func updateOrAdd*[T: VC4_Property](vc4: VCard4, content: seq[T]): VCard4 =
# Ouptut
# =============================================================================
#func serialize(p: VC4_Property): string =
# if c of
func nameWithGroup(s: VC4_Property): string =
if s.group.isSome: s.group.get & "." & s.name
else: s.name
macro genSerializers(
props: static[openarray[(VC4_PropertyName, VC4_ValueType)]]
): untyped =
result = newStmtList()
for (pn, pt) in props:
let (enumName, typeName, _, _) = namesForProp(pn)
case pt
of vtText, vtTextOrUri, vtUri, vtDateTimeOrText:
let funcDef = genAstOpt({kDirtyTemplate}, enumName, typeName):
func serialize*(p: typeName): string =
result =
p.nameWithGroup &
serialize(p.params) &
":" & serializeValue(p.value)
result.add(funcDef)
of vtTextList:
let funcDef = genAstOpt({kDirtyTemplate}, enumName, typeName):
func serialize*(p: typeName): string =
result = p.nameWithGroup & serialize(p.params) &
serialize(p.params) & ":" &
(p.value --> map(serializeValue(it))).join(",")
result.add(funcDef)
else:
raise newException(ValueError, "serializer for " & $pn &
" properties must be hand-written")
macro genGenericSerializer(props: static[openarray[VC4_PropertyName]]): untyped =
result = genAstOpt({kDirtyTemplate}):
func serialize*(c: VC4_Property): string
let ifExpr = nnkIfExpr.newTree()
for p in props:
let (_, typeName, _, _) = namesForProp(p)
let returnStmt = genAstOpt({kDirtyTemplate}, typeName):
return serialize(cast[typeName](c))
ifExpr.add(nnkElifBranch.newTree(
nnkInfix.newTree(ident("of"), ident("c"), typeName),
returnStmt))
result[6] = newStmtList(ifExpr)
func serializeParamValue(value: string): string =
result = value.multiReplace([("\n", "^n"), ("^", "^^"), ("\"", "^'")])
for c in result:
if not SAFE_CHARS.contains(c):
result = "\"" & result & "\""
break
func serialize(params: seq[VCParam]): string =
result = ""
for pLent in params:
let p = pLent
result &= ";" & p.name & "=" & (p.values -->
map(serializeParamValue(it))).join(",")
func serializeValue(value: string): string =
result = value.multiReplace(
[(",", "\\,"), (";", "\\;"), ("\\", "\\\\"),("\n", "\\n")])
func serialize*(n: VC4_N): string =
result = "N" & serialize(n.params) & ":" &
(n.family --> map(serializeValue(it))).join(",") & ";" &
(n.given --> map(serializeValue(it))).join(",") & ";" &
(n.additional --> map(serializeValue(it))).join(",") & ";" &
(n.prefixes --> map(serializeValue(it))).join(",") & ";" &
(n.suffixes --> map(serializeValue(it))).join(",")
func serialize*(a: VC4_Adr): string =
result = "ADR" & serialize(a.params) & ":" &
a.poBox & ";" & a.ext & ";" & a.street & ";" & a.locality & ";" &
a.region & ";" & a.postalCode & ";" & a.country
func serialize*(g: VC4_Gender): string =
result = "GENDER" & serialize(g.params) & ":"
if g.sex.isSome: result &= $g.sex.get
if g.genderIdentity.isSome: result &= ";" & g.genderIdentity.get
func serialize*(r: VC4_Rev): string =
result = "REV" & serialize(r.params) &
":" & r.value.format(TIMESTAMP_FORMATS[0])
func serialize*(c: VC4_ClientPidMap): string =
result = "CLIENTPIDMAP" & serialize(c.params) & ":" & $c.id & ";" & c.uri
genSerializers(fixedValueTypeProperties.toSeq & @[(pnUnknown, vtText)])
genGenericSerializer(toSeq(VC4_PropertyName))
func `$`*(pid: PidValue): string = $pid.propertyId & "." & $pid.sourceId
#[
func `$`*(vc4: VCard4): string =
result = "BEGIN:VCARD" & CRLF
result &= "VERSION:4.0" & CRLF
for p in (vc4.content --> filter(not (it of VC4_Version))):
result &= foldContentLine(serialize(p)) & CRLF
result &= "END:VCARD" & CRLF
]#
# Parsing
# =============================================================================
const TEXT_CHARS = WSP + NON_ASCII + { '\x21'..'\x2B', '\x2D'..'\x7E' }
const QSAFE_CHARS = WSP + { '\x21', '\x23'..'\x7E' } + NON_ASCII
const COMPONENT_CHARS = WSP + NON_ASCII +
{ '\x21'..'\x2B', '\x2D'..'\x3A', '\x3C'..'\x7E' }
func parsePidValues(param: VCParam): seq[PidValue]
{.raises:[VCardParsingError].} =
result = @[]
for v in param.values:
try:
let pieces = v.split(".")
if pieces.len != 2: raise newException(ValueError, "")
result.add(PidValue(
propertyId: parseInt(pieces[0]),
sourceId: parseInt(pieces[1])))
except ValueError:
raise newException(VCardParsingError, "PID value expected to be two " &
"integers separated by '.' (2.1 for example)")
proc parseTimestamp(value: string): DateTime =
for fmt in TIMESTAMP_FORMATS:
try: return value.parse(fmt)
except: discard
raise newException(VCardParsingError, "unable to parse timestamp value: " & value)
proc readParamValue(p: var VCardParser): string =
## Read a single parameter value at the current read position or error. Note
## that this implementation differs from RFC 6450 in two important ways:
@ -931,9 +1145,6 @@ proc readParamValue(p: var VCardParser): string =
p.error("quoted parameter value expected to end with a " &
"double quote (\")")
if result.len == 0:
p.error("expected to read a parameter value")
proc readParams(p: var VCardParser): seq[VCParam] =
result = @[]
while p.peek == ';':
@ -1030,7 +1241,7 @@ proc readTextValueList(
while seps.contains(p.peek): result.add(p.readTextValue(ignorePrefix = seps))
macro genPropParsers(
genProps: static[openarray[tuple[a: VC4_PropertyName, b: VC4_ValueType]]],
genProps: static[openarray[(VC4_PropertyName, VC4_ValueType)]],
group: Option[string],
name: string,
params: seq[VCParam],
@ -1049,7 +1260,7 @@ macro genPropParsers(
parseEnum[VC4_PropertyName](name, pnUnknown))
for (pn, pt) in genProps:
let (enumName, initFuncName, typeName) = namesForProp(pn)
let (enumName, typeName, initFuncName, _) = namesForProp(pn)
let parseCase = nnkOfBranch.newTree(quote do: `enumName`, newEmptyNode())
result.add(parseCase)
@ -1057,7 +1268,7 @@ macro genPropParsers(
case pt
of vtDateTimeOrText:
parseCase[1] = genAst(contents, initFuncName, typeName, p):
let valueType = params.getSingleValue("TYPE")
let valueType = params.getSingleValue("VALUE")
if valueType.isSome and valueType.get != $vtDateAndOrTime and
valueType.get != $vtText:
p.error("VALUE must be either \"date-and-or-time\" or \"text\" for " &
@ -1065,11 +1276,12 @@ macro genPropParsers(
contents.add(initFuncName(
value = p.readValue,
valueType = valueType,
group = group,
params = params))
of vtText:
parseCase[1] = genAst(contents, typeName):
parseCase[1] = genAst(contents, typeName, pt):
p.validateType(params, pt)
contents.add(ac(typeName(value: p.readTextValue)))
@ -1080,17 +1292,17 @@ macro genPropParsers(
of vtTextOrUri:
parseCase[1] = genAst(contents, typeName):
let valueType = params.getSingleValue("TYPE")
let valueType = params.getSingleValue("VALUE")
if valueType.isNone or valueType.get == $vtUri:
contents.add(ac(typeName(value: p.readValue)))
elif valueType.isSome and valueType.get == $vtText:
contents.add(ac(typeName(value: p.readTextValue)))
else:
p.error("VALUE must be either \"text\" or \"uri\" for " & name &
" properties.")
p.error(("VALUE must be either \"text\" or \"uri\" for $# " &
"properties (was $#).") % [name, $valueType])
of vtUri:
parseCase[1] = genAst(typeName, contents):
parseCase[1] = genAst(typeName, contents, pt):
p.validateType(params, pt)
contents.add(ac(typeName(value: p.readValue)))
@ -1126,11 +1338,11 @@ macro genPropParsers(
contents.add(ac(VC4_Gender(
sex: sex,
gender:
if sexCh == ';' or sex.isSome: some(p.readTextValue)
genderIdentity:
if sexCh == ';' or p.peek == ';':
some(p.readTextValue(ignorePrefix = {';'}))
else: none[string]())))
block: # ADR
let parseCase = nnkOfBranch.newTree(ident("pnAdr"), newEmptyNode())
result.add(parseCase)
@ -1164,7 +1376,7 @@ macro genPropParsers(
let parseCase = nnkOfBranch.newTree(ident("pnUnknown"), newEmptyNode())
result.add(parseCase)
parseCase[1] = genAst(contents):
contents.add(ac(VC4_UnknownProperty(name: name, value: p.readValue)))
contents.add(ac(VC4_Unknown(name: name, value: p.readValue)))
# echo result.repr
proc parseContentLines*(p: var VCardParser): seq[VC4_Property] =
@ -1181,40 +1393,7 @@ proc parseContentLines*(p: var VCardParser): seq[VC4_Property] =
genPropParsers(fixedValueTypeProperties, group, name, params, result, p)
#[
of $pnNickname:
of $pnPhoto:
of $pnBday:
of $pnAnniversary:
of $pnGender:
of $pnAdr:
of $pnTel:
of $pnEmail:
of $pnImpp:
of $pnLang:
of $pnTz:
of $pnGeo:
of $pnTitle:
of $pnRole:
of $pnLogo:
of $pnOrg:
of $pnMember:
of $pnRelated:
of $pnCategories:
of $pnNote:
of $pnProdId:
of $pnRev:
of $pnSound:
of $pnUid:
of $pnClientPidMap:
of $pnUrl:
of $pnVersion:
of $pnKey:
of $pnFbUrl:
of $pnCaladrUri:
of $pnCalUri:
]#
#else: discard
p.expect(CRLF)
# Private Function Unit Tests

File diff suppressed because one or more lines are too long

212
tests/allen.foster.v4.vcf Normal file
View File

@ -0,0 +1,212 @@
BEGIN:VCARD
VERSION:4.0
PRODID:+//IDN bitfire.at//DAVx5/4.1.1-gplay ez-vcard/0.11.3
UID:b7047a2e-c46b-47cb-af0b-94d354b7746a
FN:Dr. Allen Foster
N;SORT-AS=Foster,Allen,,,:Foster;Jack;John,Allen;Dr.;II
NICKNAME:Jack Jr.
NICKNAME;TYPE=work;PREF=1:Doc A
TEL;TYPE=cell:+1 555-123-4567
TEL;TYPE=cell:(555) 123-4567
TEL;TYPE=work,voice;VALUE=uri:tel:+1-555-874-1234
EMAIL;TYPE=work;PREF=2:jack.foster@company.test
EMAIL;TYPE=home;PREF=1:allen@fosters.test
SOURCE;VALUE=uri:https://carddav.fosters.test/allen.vcf
KIND:individual
REV:20220226T060828Z
BDAY;ALTID=1;VALUE=date-and-or-time:--1224
BDAY;ALTID=1;VALUE=text:Christmas Eve
ANNIVERSARY:20140612T163000-0500
GENDER:M;male
MADE-UP-PROP:Sample value for my made-up prop.
NOTE;LANG=en-us:This is an example\, for clarity; in text value cases the parser
will recognize escape values for '\,'\, '\\'\, and newlines. For example:\n 12
3 Flagstaff Road\N Placeville\, MA
X-CUSTOM-EXAMPLE;PARAM="How one says, ^'Hello.^'";LABEL=^^top^nsecond line:This
is an example, for clarity; in straight value cases, the parser does not reco
gnize any escape values, as the meaning of the content is implementation-speci
fic.
PHOTO;ALTID=1;VALUE=uri:https://tile.loc.gov/storage-services/service/pnp/
bellcm/02200/02297r.jpg
URL:https://allen.fosters.test/
PHOTO;ALTID=1:data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEFeAV4AAD/4QCuRXhpZgAASU
kqAAgAAAAHABIBAwABAAAAAQAAABoBBQABAAAAYgAAABsBBQABAAAAagAAACgBAwABAAAAAgAAADEB
AgANAAAAcgAAADIBAgAUAAAAgAAAAGmHBAABAAAAlAAAAAAAAAB4BQAAAQAAAHgFAAABAAAAR0lNUC
AyLjEwLjM0AAAyMDIzOjA0OjI1IDE2OjQzOjUyAAEAAaADAAEAAAABAAAAAAAAAP/hDM1odHRwOi8v
bnMuYWRvYmUuY29tL3hhcC8xLjAvADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaU
h6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1w
dGs9IlhNUCBDb3JlIDQuNC4wLUV4aXYyIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3Ln
czLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJv
dXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOn
N0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHht
bG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6R0lNUD0iaHR0cD
ovL3d3dy5naW1wLm9yZy94bXAvIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEu
MC8iIHhtcE1NOkRvY3VtZW50SUQ9ImdpbXA6ZG9jaWQ6Z2ltcDpmYzVkZDFkMC05ZmNiLTRhZjAtOG
UzNS1jMTMyMDU4NTUwMmEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6YmEyOThkOGYtMDY3NC00
ZDgzLWJhZGMtNWVkY2Y2OTg2NTBjIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6Nz
JjYWViYjMtMjkzMy00ZGJmLTg0M2EtYzYwYjBkZWYzMzdlIiBkYzpGb3JtYXQ9ImltYWdlL2pwZWci
IEdJTVA6QVBJPSIyLjAiIEdJTVA6UGxhdGZvcm09IldpbmRvd3MiIEdJTVA6VGltZVN0YW1wPSIxNj
gyNDU5MDQ2MjA4NjE3IiBHSU1QOlZlcnNpb249IjIuMTAuMzQiIHhtcDpDcmVhdG9yVG9vbD0iR0lN
UCAyLjEwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIzOjA0OjI1VDE2OjQzOjUyLTA1OjAwIiB4bXA6TW
9kaWZ5RGF0ZT0iMjAyMzowNDoyNVQxNjo0Mzo1Mi0wNTowMCI+IDx4bXBNTTpIaXN0b3J5PiA8cmRm
OlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDpjaGFuZ2VkPSIvIiBzdEV2dD
ppbnN0YW5jZUlEPSJ4bXAuaWlkOjU2YzcxYmUyLWExNmMtNDE2OC1iNDA5LWI3YjRlMTgwZTFmMyIg
c3RFdnQ6c29mdHdhcmVBZ2VudD0iR2ltcCAyLjEwIChXaW5kb3dzKSIgc3RFdnQ6d2hlbj0iMjAyMy
0wNC0yNVQxNjo0NDowNiIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3Jp
cHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+ICAgICAgICAgICAgICAgICAgICAgICAgICAgIC
AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC
AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC
AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC
AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC
AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC
AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC
AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC
AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC
AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC
AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC
AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC
AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC
AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC
AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC
AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC
AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC
AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC
AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPD94cGFja2V0IGVuZD0idyI/Pv/iAjBJQ0Nf
UFJPRklMRQABAQAAAiBsY21zBEAAAG1udHJHUkFZWFlaIAfnAAQAGQAVACoAJWFjc3BNU0ZUAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAD21gABAAAAANMtbGNtcwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmRlc2MAAADMAAAAbmNwcnQAAAE8AAAANnd0cHQAAAF0AA
AAFGtUUkMAAAGIAAAAIGRtbmQAAAGoAAAAJGRtZGQAAAHMAAAAUm1sdWMAAAAAAAAAAQAAAAxlblVT
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
GAaDKL5DfxBTTUUbUr1Zb/APP/AP/Z
END:VCARD

View File

@ -1,4 +1,5 @@
import options, unittest, zero_functional
import std/[options, strutils, tables, unittest]
import zero_functional
import ./vcard
import ./vcard/vcard4
@ -7,3 +8,243 @@ suite "vcard/vcard4":
test "vcard4/private tests":
runVcard4PrivateTests()
let v4ExampleStr = readFile("tests/allen.foster.v4.vcf")
let testVCardTemplate =
"BEGIN:VCARD\r\n" &
"VERSION:4.0\r\n" &
"$#" &
"END:VCARD\r\n"
test "parseVCard4":
check parseVCards(v4ExampleStr).len == 1
test "parseVCard4File":
check parseVCardsFromFile("tests/allen.foster.v4.vcf").len == 1
# TODO: remove cast after finishing VCard4 implementation
let v4Ex = cast[VCard4](parseVCards(v4ExampleStr)[0])
test "RFC 6350 author's VCard":
let vcardStr =
"BEGIN:VCARD\r\n" &
"VERSION:4.0\r\n" &
"FN:Simon Perreault\r\n" &
"N:Perreault;Simon;;;ing. jr,M.Sc.\r\n" &
"BDAY:--0203\r\n" &
"ANNIVERSARY:20090808T1430-0500\r\n" &
"GENDER:M\r\n" &
"LANG;PREF=1:fr\r\n" &
"LANG;PREF=2:en\r\n" &
"ORG;TYPE=work:Viagenie\r\n" &
"ADR;TYPE=work:;Suite D2-630;2875 Laurier;\r\n" &
" Quebec;QC;G1V 2M2;Canada\r\n" &
"TEL;VALUE=uri;TYPE=\"work,voice\";PREF=1:tel:+1-418-656-9254;ext=102\r\n" &
"TEL;VALUE=uri;TYPE=\"work,cell,voice,video,text\":tel:+1-418-262-6501\r\n" &
"EMAIL;TYPE=work:simon.perreault@viagenie.ca\r\n" &
"GEO;TYPE=work:geo:46.772673,-71.282945\r\n" &
"KEY;TYPE=work;VALUE=uri:\r\n" &
" http://www.viagenie.ca/simon.perreault/simon.asc\r\n" &
"TZ:-0500\r\n" &
"URL;TYPE=home:http://nomis80.org\r\n" &
"END:VCARD\r\n"
let vcards = parseVCards(vcardStr)
check vcards.len == 1
let sp = cast[VCard4](vcards[0])
check:
sp.fn.len == 1
sp.fn[0].value == "Simon Perreault"
sp.gender.isSome
sp.gender.get.sex == some(VC4_Sex.Male)
sp.gender.get.genderIdentity.isNone
sp.lang.len == 2
sp.lang --> map(it.value) == @["fr", "en"]
test "custom properties are serialized":
let email = newVC4_Email(
value ="john.smith@testco.test",
types = @["work", "internet"],
params = @[("PREF", @["1"]), ("X-ATTACHMENT-LIMIT", @["25MB"])])
check serialize(email) ==
"EMAIL;X-ATTACHMENT-LIMIT=25MB;TYPE=work,internet;PREF=1:john.smith@testco.test"
test "can parse properties with escaped characters":
check v4Ex.note.len == 1
let note = v4Ex.note[0]
check note.value ==
"This is an example, for clarity; in text value cases the parser " &
"will recognize escape values for ',', '\\', and newlines. For " &
"example:" &
"\n\t123 Flagstaff Road" &
"\n\tPlaceville, MA"
test "can parse parameters with escaped characters":
let prop = v4Ex.customProp("X-CUSTOM-EXAMPLE")[0]
check prop.value ==
"This is an example, for clarity; in straight value cases, the parser " &
"does not recognize any escape values, as the meaning of the content " &
"is implementation-specific."
let param1 = prop.params --> filter(it.name == "PARAM")
let label = prop.params --> filter(it.name == "LABEL")
check:
param1.len == 1
param1[0].values == @["How one says, \"Hello.\""]
label.len == 1
label[0].values == @["^top\nsecond line"]
test "Data URIs are parsed correctly":
let expectedB64 = readFile("tests/allen.foster.jpg.uri")
check:
v4Ex.photo.len == 2
v4Ex.photo[0].altId == some("1")
v4Ex.photo[0].value ==
"https://tile.loc.gov/storage-services/service/pnp/bellcm/02200/02297r.jpg"
v4Ex.photo[0].valueType == some("uri")
v4Ex.photo[1].altId == some("1")
v4Ex.photo[1].value == expectedB64
v4Ex.photo[1].valueType.isNone
test "URI-type properties are parsed correctly":
# Covers SOURCE, PHOTO, IMPP, GEO, LOGO, MEMBER, SOUND, URL, FBURL,
# CALADRURI, and CALURI
check:
v4Ex.source.len == 1
v4Ex.source[0].value == "https://carddav.fosters.test/allen.vcf"
v4Ex.source[0].valueType == some("uri")
v4Ex.url.len == 1
v4Ex.url[0].value == "https://allen.fosters.test/"
test "URI-type properties are serialized correctly":
# Covers SOURCE, PHOTO, IMPP, GEO, LOGO, MEMBER, SOUND, URL, FBURL,
# CALADRURI, and CALURI
let src = newVC4_Source(value="https://carddav.example.test/john-smith.vcf")
check serialize(src) == "SOURCE:https://carddav.example.test/john-smith.vcf"
test "Single-text properties are parsed correctly":
# Covers KIND, XML, FN, NICKNAME, EMAIL, LANG, TZ, TITLE, ROLE, ORG, NOTE,
# PRODID, and VERSION
check:
v4Ex.kind.isSome
v4Ex.kind.get.value == "individual"
v4Ex.nickname.len == 2
v4Ex.nickname[0].value == @["Jack Jr."]
v4Ex.nickname[1].value == @["Doc A"]
v4Ex.fn.len == 1
v4Ex.fn[0].value == "Dr. Allen Foster"
v4Ex.email.len == 2
v4Ex.email[0].value == "jack.foster@company.test"
v4Ex.email[0].types == @["work"]
test "URI or Text properties are parsed correctly":
# Covers TEL, RELATED, UID, KEY
check:
v4Ex.tel.len == 3
v4ex.tel[0].types == @[$VC4_TelType.ttCell]
v4Ex.tel[0].value == "+1 555-123-4567"
v4Ex.tel[2].types == @[$VC4_TelType.ttWork,$VC4_TelType.ttVoice]
v4Ex.tel[2].valueType == some($vtUri)
v4Ex.tel[2].value == "tel:+1-555-874-1234"
test "N is parsed correctly":
check:
v4Ex.n.isSome
v4Ex.n.get.given == @["Jack"]
v4Ex.n.get.family == @["Foster"]
v4Ex.n.get.additional == @["John", "Allen"]
v4Ex.n.get.prefixes == @["Dr."]
v4Ex.n.get.suffixes == @["II"]
test "BDAY is parsed correctly":
check:
v4Ex.bday.isSome
v4Ex.bday.get.value == "--1224"
v4Ex.bday.get.year.isNone
v4Ex.bday.get.month == some(12)
v4Ex.bday.get.day == some(24)
test "ANNIVERSARY is parsed correctly":
check:
v4Ex.anniversary.isSome
v4Ex.anniversary.get.value == "20140612T163000-0500"
v4Ex.anniversary.get.year == some(2014)
v4Ex.anniversary.get.hour == some(16)
v4Ex.anniversary.get.minute == some(30)
v4Ex.anniversary.get.timezone == some("-0500")
test "GENDER is parsed correctly":
check:
v4Ex.gender.isSome
v4Ex.gender.get.sex == some(VC4_Sex.Male)
v4Ex.gender.get.genderIdentity == some("male")
#[
test "CATEGORIES is parsed correctly":
test "REV is parsed correctly":
test "CLIENTPIDMAP is parsed correctly":
]#
test "unknown properties are parsed correctly":
check v4Ex.customProp("MADE-UP-PROP").len == 1
let madeUpProp = v4Ex.customProp("MADE-UP-PROP")[0]
check:
madeUpProp.name == "MADE-UP-PROP"
madeUpProp.value == "Sample value for my made-up prop."
let cardWithAltBdayStr = testVCardTemplate % [(
"BDAY;VALUE=text;ALTID=1:20th century\r\n" &
"BDAY;VALUE=date-and-or-time;ALTID=1:19650321\r\n"
)]
test "single-cardinality properties allow multiples with ALTID":
check parseVCards(cardWithAltBdayStr).len == 1
let hasAltBdays = cast[VCard4](parseVCards(cardWithAltBdayStr)[0])
test "properties with cardinality 1 and altids return the first found by default":
check:
hasAltBdays.bday.isSome
hasAltBdays.bday.get.value == "20th century"
hasAltBdays.bday.get.year.isNone
test "allAlternatives":
check:
hasAltBdays.content.len == 3
hasAltBdays.bday.isSome
let allBdays = allAlternatives[VC4_Bday](hasAltBdays)
check:
allBdays.len == 1
allBdays.contains("1")
allBdays["1"].len == 2
let bday0 = allBdays["1"][0]
check:
bday0.value == "20th century"
bday0.year.isNone
bday0.month.isNone
bday0.day.isNone
bday0.hour.isNone
bday0.minute.isNone
bday0.second.isNone
bday0.timezone.isNone
let bday1 = allBDays["1"][1]
check:
bday1.value == "19650321"
bday1.year == some(1965)
bday1.month == some(3)
bday1.day == some(21)
bday1.hour.isNone
bday1.minute.isNone
bday1.second.isNone
test "PREF ordering":
check:
v4Ex.nickname --> map(it.value) == @[@["Jack Jr."], @["Doc A"]]
v4Ex.nickname.inPrefOrder --> map(it.value) == @[@["Doc A"], @["Jack Jr."]]