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:
parent
daa58518e3
commit
98c300fee2
@ -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
|
import zero_functional
|
||||||
|
|
||||||
from std/sequtils import toSeq
|
#from std/sequtils import toSeq
|
||||||
|
|
||||||
import ./private/[common, lexer, util]
|
import ./private/[common, lexer, util]
|
||||||
|
|
||||||
type
|
type
|
||||||
VC4_Cardinality = enum
|
VC4_ValueType* = enum
|
||||||
vccAtMostOne,
|
|
||||||
vccExactlyOne,
|
|
||||||
vccAtLeastOne
|
|
||||||
vccAny
|
|
||||||
|
|
||||||
VC4_ValueType = enum
|
|
||||||
vtText = "text",
|
vtText = "text",
|
||||||
vtTextList = "text-list",
|
vtTextList = "text-list",
|
||||||
vtUri = "uri",
|
vtUri = "uri",
|
||||||
@ -74,43 +69,43 @@ type
|
|||||||
## Non-standard, added to be a catch-all for non-standard names
|
## Non-standard, added to be a catch-all for non-standard names
|
||||||
pnUnknown = "UNKNOWN"
|
pnUnknown = "UNKNOWN"
|
||||||
|
|
||||||
const propertyCardMap: Table[VC4_PropertyName, VC4_Cardinality] = [
|
const propertyCardMap: Table[VC4_PropertyName, VC_PropCardinality] = [
|
||||||
(pnSource, vccAny),
|
(pnSource, vpcAny),
|
||||||
(pnKind, vccAtMostOne),
|
(pnKind, vpcAtMostOne),
|
||||||
(pnXml, vccAny),
|
(pnXml, vpcAny),
|
||||||
(pnFn, vccAtLeastOne),
|
(pnFn, vpcAtLeastOne),
|
||||||
(pnN, vccAtMostOne),
|
(pnN, vpcAtMostOne),
|
||||||
(pnNickname, vccAny),
|
(pnNickname, vpcAny),
|
||||||
(pnPhoto, vccAny),
|
(pnPhoto, vpcAny),
|
||||||
(pnBday, vccAtMostOne),
|
(pnBday, vpcAtMostOne),
|
||||||
(pnAnniversary, vccAtMostOne),
|
(pnAnniversary, vpcAtMostOne),
|
||||||
(pnGender, vccAtMostOne),
|
(pnGender, vpcAtMostOne),
|
||||||
(pnAdr, vccAny),
|
(pnAdr, vpcAny),
|
||||||
(pnTel, vccAny),
|
(pnTel, vpcAny),
|
||||||
(pnEmail, vccAny),
|
(pnEmail, vpcAny),
|
||||||
(pnImpp, vccAny),
|
(pnImpp, vpcAny),
|
||||||
(pnLang, vccAny),
|
(pnLang, vpcAny),
|
||||||
(pnTz, vccAny),
|
(pnTz, vpcAny),
|
||||||
(pnGeo, vccAny),
|
(pnGeo, vpcAny),
|
||||||
(pnTitle, vccAny),
|
(pnTitle, vpcAny),
|
||||||
(pnRole, vccAny),
|
(pnRole, vpcAny),
|
||||||
(pnLogo, vccAny),
|
(pnLogo, vpcAny),
|
||||||
(pnOrg, vccAny),
|
(pnOrg, vpcAny),
|
||||||
(pnMember, vccAny),
|
(pnMember, vpcAny),
|
||||||
(pnRelated, vccAny),
|
(pnRelated, vpcAny),
|
||||||
(pnCategories, vccAny),
|
(pnCategories, vpcAny),
|
||||||
(pnNote, vccAny),
|
(pnNote, vpcAny),
|
||||||
(pnProdId, vccAtMostOne),
|
(pnProdId, vpcAtMostOne),
|
||||||
(pnRev, vccAtMostOne),
|
(pnRev, vpcAtMostOne),
|
||||||
(pnSound, vccAny),
|
(pnSound, vpcAny),
|
||||||
(pnUid, vccAtMostOne),
|
(pnUid, vpcAtMostOne),
|
||||||
(pnClientPidMap, vccAny),
|
(pnClientPidMap, vpcAny),
|
||||||
(pnUrl, vccAny),
|
(pnUrl, vpcAny),
|
||||||
(pnVersion, vccExactlyOne),
|
(pnVersion, vpcExactlyOne),
|
||||||
(pnKey, vccAny),
|
(pnKey, vpcAny),
|
||||||
(pnFbUrl, vccAny),
|
(pnFbUrl, vpcAny),
|
||||||
(pnCaladrUri, vccAny),
|
(pnCaladrUri, vpcAny),
|
||||||
(pnCalUri, vccAny)
|
(pnCalUri, vpcAny)
|
||||||
].toTable()
|
].toTable()
|
||||||
|
|
||||||
const fixedValueTypeProperties = [
|
const fixedValueTypeProperties = [
|
||||||
@ -156,25 +151,26 @@ const fixedValueTypeProperties = [
|
|||||||
(pnCalUri, vtUri)
|
(pnCalUri, vtUri)
|
||||||
]
|
]
|
||||||
|
|
||||||
const supportedParameters: Table[string, HashSet[VC4_PropertyName]] = [
|
const supportedParams: Table[string, HashSet[VC4_PropertyName]] = [
|
||||||
("LANGUAGE", [pnFn, pnN, pnNickname, pnBday, pnAdr, pnTitle, pnRole,
|
("LANGUAGE", [pnFn, pnN, pnNickname, pnBday, pnAdr, pnTitle, pnRole,
|
||||||
pnLogo, pnOrg, pnRelated, pnNote, pnSound].toHashSet),
|
pnLogo, pnOrg, pnRelated, pnNote, pnSound].toHashSet),
|
||||||
|
|
||||||
("PREF", (propertyCardMap.pairs.toSeq -->
|
("PREF", (propertyCardMap.pairs.toSeq -->
|
||||||
filter(it[1] == vccAtLeastOne or it[1] == vccAny).
|
filter(it[1] == vpcAtLeastOne or it[1] == vpcAny).
|
||||||
map(it[0])).
|
map(it[0])).
|
||||||
toHashSet),
|
toHashSet),
|
||||||
|
|
||||||
# ("ALTID", all properties),
|
# ("ALTID", all properties),
|
||||||
|
|
||||||
("PID", (propertyCardMap.pairs.toSeq -->
|
("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).
|
it[0] != pnClientPidMap).
|
||||||
map(it[0])).toHashSet),
|
map(it[0])).toHashSet),
|
||||||
|
|
||||||
("TYPE", @[ pnFn, pnNickname, pnPhoto, pnAdr, pnTel, pnEmail, pnImpp, pnLang,
|
("TYPE", @[ pnFn, pnNickname, pnPhoto, pnAdr, pnTel, pnEmail, pnImpp, pnLang,
|
||||||
pnTz, pnGeo, pnTitle, pnRole, pnLogo, pnOrg, pnRelated, pnCategories,
|
pnTz, pnGeo, pnTitle, pnRole, pnLogo, pnOrg, pnRelated, pnCategories,
|
||||||
pnNote, pnSound, pnUrl, pnKey, pnFburl, pnCaladrUri, pnCalUri ].toHashSet),
|
pnNote, pnSound, pnUrl, pnKey, pnFburl, pnCaladrUri, pnCalUri ].toHashSet),
|
||||||
|
|
||||||
].toTable
|
].toTable
|
||||||
|
|
||||||
const TIMESTAMP_FORMATS = [
|
const TIMESTAMP_FORMATS = [
|
||||||
@ -184,8 +180,12 @@ const TIMESTAMP_FORMATS = [
|
|||||||
"yyyyMMdd'T'hhmmss"
|
"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(
|
macro genPropTypes(
|
||||||
props: static[openarray[ tuple[a: VC4_PropertyName, b: VC4_ValueType]]]
|
props: static[openarray[(VC4_PropertyName, VC4_ValueType)]]
|
||||||
): untyped =
|
): untyped =
|
||||||
|
|
||||||
result = newNimNode(nnkTypeSection)
|
result = newNimNode(nnkTypeSection)
|
||||||
@ -230,7 +230,6 @@ type
|
|||||||
group*: Option[string]
|
group*: Option[string]
|
||||||
params*: seq[VCParam]
|
params*: seq[VCParam]
|
||||||
|
|
||||||
# TODO: write accessors
|
|
||||||
VC4_DateTimeOrTextProperty* = ref object of VC4_Property
|
VC4_DateTimeOrTextProperty* = ref object of VC4_Property
|
||||||
valueType: VC4_ValueType # should only be vtDateAndOrTime or vtText
|
valueType: VC4_ValueType # should only be vtDateAndOrTime or vtText
|
||||||
value*: string
|
value*: string
|
||||||
@ -259,7 +258,7 @@ type
|
|||||||
VC4_DateTimeProperty* = ref object of VC4_Property
|
VC4_DateTimeProperty* = ref object of VC4_Property
|
||||||
value*: DateTime
|
value*: DateTime
|
||||||
|
|
||||||
VC4_UnknownProperty* = ref object of VC4_Property
|
VC4_Unknown* = ref object of VC4_Property
|
||||||
name*: string
|
name*: string
|
||||||
value*: string
|
value*: string
|
||||||
|
|
||||||
@ -332,7 +331,7 @@ type
|
|||||||
|
|
||||||
VC4_Gender* = ref object of VC4_Property
|
VC4_Gender* = ref object of VC4_Property
|
||||||
sex*: Option[VC4_Sex]
|
sex*: Option[VC4_Sex]
|
||||||
gender*: Option[string]
|
genderIdentity*: Option[string]
|
||||||
|
|
||||||
VC4_Adr* = ref object of VC4_Property
|
VC4_Adr* = ref object of VC4_Property
|
||||||
poBox*: string
|
poBox*: string
|
||||||
@ -366,11 +365,9 @@ func flattenParameters(
|
|||||||
): seq[VCParam] =
|
): seq[VCParam] =
|
||||||
|
|
||||||
let paramTable = newTable[string, seq[string]]()
|
let paramTable = newTable[string, seq[string]]()
|
||||||
let allParams: seq[VCParam] =
|
let allParams = params & toSeq(addtlParams)
|
||||||
params &
|
|
||||||
addtlParams.toSeq --> filter(it.values.len > 0)
|
|
||||||
|
|
||||||
for p in allParams:
|
for p in (allParams --> filter(it.values.len > 0)):
|
||||||
let pname = p.name.toUpper
|
let pname = p.name.toUpper
|
||||||
if paramTable.contains(pname):
|
if paramTable.contains(pname):
|
||||||
for v in p.values:
|
for v in p.values:
|
||||||
@ -388,6 +385,7 @@ proc parseDateAndOrTime[T](
|
|||||||
value: string
|
value: string
|
||||||
): void =
|
): void =
|
||||||
|
|
||||||
|
prop.value = value
|
||||||
var p = VCardParser(filename: value)
|
var p = VCardParser(filename: value)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -404,30 +402,32 @@ proc parseDateAndOrTime[T](
|
|||||||
if p.peek != 'T':
|
if p.peek != 'T':
|
||||||
# Attempt to parse the year
|
# Attempt to parse the year
|
||||||
if p.peek == '-': p.expect("--")
|
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
|
# Attempt to parse the month
|
||||||
if DIGIT.contains(p.peek) or p.peek == '-':
|
if DIGIT.contains(p.peek) or p.peek == '-':
|
||||||
if p.peek == '-': p.expect("-")
|
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
|
# Attempt to parse the month
|
||||||
if DIGIT.contains(p.peek):
|
if DIGIT.contains(p.peek):
|
||||||
prop.day = some(parseInt(p.read & p.read))
|
prop.day = some(parseInt(p.readLen(2)))
|
||||||
|
|
||||||
if p.peek == 'T':
|
if p.peek == 'T':
|
||||||
|
p.expect("T")
|
||||||
|
|
||||||
# Attempt to parse the hour
|
# Attempt to parse the hour
|
||||||
if p.peek == '-': p.expect("-")
|
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
|
# Attempt to parse the minute
|
||||||
if DIGIT.contains(p.peek) or p.peek == '-':
|
if DIGIT.contains(p.peek) or p.peek == '-':
|
||||||
if p.peek == '-': p.expect("-")
|
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
|
# Attempt to parse the second
|
||||||
if DIGIT.contains(p.peek):
|
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
|
# Attempt to parse the timezone
|
||||||
if {'-', '+', 'Z'}.contains(p.peek):
|
if {'-', '+', 'Z'}.contains(p.peek):
|
||||||
@ -445,9 +445,33 @@ proc parseDateAndOrTime[T](
|
|||||||
p.error("unable to parse date-and-or-time value: " & p.readSinceBookmark)
|
p.error("unable to parse date-and-or-time value: " & p.readSinceBookmark)
|
||||||
finally: p.unsetBookmark
|
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) =
|
template validateType(p: VCardParser, params: seq[VCParam], t: VC4_ValueType) =
|
||||||
p.validateRequiredParameters(params, [("VALUE", $t)])
|
p.validateRequiredParameters(params, [("VALUE", $t)])
|
||||||
|
|
||||||
|
func cmp[T: VC4_Property](x, y: T): int =
|
||||||
|
return cmp(x.pref, y.pref)
|
||||||
|
|
||||||
# Initializers
|
# Initializers
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@ -481,7 +505,7 @@ func addConditionalParams(prop: VC4_PropertyName, funcDef: NimNode) =
|
|||||||
raise newException(ValueError, "cannot generate conditional params " &
|
raise newException(ValueError, "cannot generate conditional params " &
|
||||||
"initialization code for this function shape:\n\r " & funcDef.treeRepr)
|
"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
|
# Add "language" as a function parameter
|
||||||
formalParams.add(newIdentDefs(
|
formalParams.add(newIdentDefs(
|
||||||
ident("language"),
|
ident("language"),
|
||||||
@ -491,7 +515,7 @@ func addConditionalParams(prop: VC4_PropertyName, funcDef: NimNode) =
|
|||||||
paramsInit.add(quote do:
|
paramsInit.add(quote do:
|
||||||
("LANGUAGE", if language.isSome: @[language.get] else: @[]))
|
("LANGUAGE", if language.isSome: @[language.get] else: @[]))
|
||||||
|
|
||||||
if supportedParameters["PREF"].contains(prop):
|
if supportedParams["PREF"].contains(prop):
|
||||||
# Add "pref" and "pids" as function parameters
|
# Add "pref" and "pids" as function parameters
|
||||||
formalParams.add(newIdentDefs(
|
formalParams.add(newIdentDefs(
|
||||||
ident("pref"),
|
ident("pref"),
|
||||||
@ -502,7 +526,7 @@ func addConditionalParams(prop: VC4_PropertyName, funcDef: NimNode) =
|
|||||||
("PREF", if pref.isSome: @[$pref.get] else: @[]))
|
("PREF", if pref.isSome: @[$pref.get] else: @[]))
|
||||||
|
|
||||||
|
|
||||||
if supportedParameters["PID"].contains(prop):
|
if supportedParams["PID"].contains(prop):
|
||||||
# Add "pids" as a function parameter
|
# Add "pids" as a function parameter
|
||||||
formalParams.add(newIdentDefs(
|
formalParams.add(newIdentDefs(
|
||||||
ident("pids"),
|
ident("pids"),
|
||||||
@ -514,7 +538,7 @@ func addConditionalParams(prop: VC4_PropertyName, funcDef: NimNode) =
|
|||||||
paramsInit.add(quote do: ("PID", pids --> map($it)))
|
paramsInit.add(quote do: ("PID", pids --> map($it)))
|
||||||
|
|
||||||
|
|
||||||
if supportedParameters["TYPE"].contains(prop):
|
if supportedParams["TYPE"].contains(prop):
|
||||||
# Add "type" as a function parameter
|
# Add "type" as a function parameter
|
||||||
formalParams.add(newIdentDefs(
|
formalParams.add(newIdentDefs(
|
||||||
ident("types"),
|
ident("types"),
|
||||||
@ -526,21 +550,29 @@ func addConditionalParams(prop: VC4_PropertyName, funcDef: NimNode) =
|
|||||||
paramsInit.add(quote do: ("TYPE", types))
|
paramsInit.add(quote do: ("TYPE", types))
|
||||||
|
|
||||||
func namesForProp(prop: VC4_PropertyName):
|
func namesForProp(prop: VC4_PropertyName):
|
||||||
tuple[enumName, funcName, typeName: NimNode] =
|
tuple[enumName, typeName, initFuncName, accessorName: NimNode] =
|
||||||
|
|
||||||
var name: string = $prop
|
var name: string = $prop
|
||||||
if name.len > 1: name = name[0] & name[1..^1].toLower
|
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(
|
macro genDateTimeOrTextPropInitializers(
|
||||||
properties: static[openarray[VC4_PropertyName]]
|
properties: static[openarray[VC4_PropertyName]]
|
||||||
): untyped =
|
): 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()
|
result = newStmtList()
|
||||||
for prop in properties:
|
for prop in properties:
|
||||||
let (enumName, funcName, typeName) = namesForProp(prop)
|
let (enumName, typeName, initFuncName, _) = namesForProp(prop)
|
||||||
let datetimeFuncDef = genAstOpt({kDirtyTemplate}, enumName, funcName, typeName):
|
let datetimeFuncDef = genAstOpt({kDirtyTemplate}, enumName, initFuncName, typeName):
|
||||||
func funcName*(
|
func initFuncName*(
|
||||||
value: DateTime,
|
value: DateTime,
|
||||||
altId: Option[string] = none[string](),
|
altId: Option[string] = none[string](),
|
||||||
group: Option[string] = none[string](),
|
group: Option[string] = none[string](),
|
||||||
@ -559,9 +591,10 @@ macro genDateTimeOrTextPropInitializers(
|
|||||||
timezone: some(value.format("ZZZ")),
|
timezone: some(value.format("ZZZ")),
|
||||||
valueType: vtDateAndOrTime)
|
valueType: vtDateAndOrTime)
|
||||||
|
|
||||||
let textFuncDef = genAstOpt({kDirtyTemplate}, enumName, funcName, typeName):
|
let textFuncDef = genAstOpt({kDirtyTemplate}, enumName, initFuncName, typeName):
|
||||||
proc funcName*(
|
proc initFuncName*(
|
||||||
value: string,
|
value: string,
|
||||||
|
valueType: Option[string] = some($vtDateAndOrTime),
|
||||||
altId: Option[string] = none[string](),
|
altId: Option[string] = none[string](),
|
||||||
group: Option[string] = none[string](),
|
group: Option[string] = none[string](),
|
||||||
params: seq[VCParam] = @[]): typeName =
|
params: seq[VCParam] = @[]): typeName =
|
||||||
@ -572,7 +605,8 @@ macro genDateTimeOrTextPropInitializers(
|
|||||||
value: value,
|
value: value,
|
||||||
valueType: vtText)
|
valueType: vtText)
|
||||||
|
|
||||||
result.parseDateAndOrTime(value)
|
if valueType.isNone or valueType.get == $vtDateAndOrTime:
|
||||||
|
result.parseDateAndOrTime(value)
|
||||||
|
|
||||||
addConditionalParams(prop, datetimeFuncDef)
|
addConditionalParams(prop, datetimeFuncDef)
|
||||||
addConditionalParams(prop, textFuncDef)
|
addConditionalParams(prop, textFuncDef)
|
||||||
@ -585,9 +619,9 @@ macro genTextPropInitializers(
|
|||||||
|
|
||||||
result = newStmtList()
|
result = newStmtList()
|
||||||
for prop in properties:
|
for prop in properties:
|
||||||
let (enumName, funcName, typeName) = namesForProp(prop)
|
let (enumName, typeName, initFuncName, _) = namesForProp(prop)
|
||||||
let funcDef = genAstOpt({kDirtyTemplate}, enumName, funcName, typeName):
|
let funcDef = genAstOpt({kDirtyTemplate}, enumName, initFuncName, typeName):
|
||||||
func funcName*(
|
func initFuncName*(
|
||||||
value: string,
|
value: string,
|
||||||
altId: Option[string] = none[string](),
|
altId: Option[string] = none[string](),
|
||||||
group: Option[string] = none[string](),
|
group: Option[string] = none[string](),
|
||||||
@ -608,9 +642,9 @@ macro genTextListPropInitializers(
|
|||||||
result = newStmtList()
|
result = newStmtList()
|
||||||
|
|
||||||
for prop in properties:
|
for prop in properties:
|
||||||
let (enumName, funcName, typeName) = namesForProp(prop)
|
let (enumName, typeName, initFuncName, _) = namesForProp(prop)
|
||||||
let funcDef = genAstOpt({kDirtyTemplate}, enumName, funcName, typeName):
|
let funcDef = genAstOpt({kDirtyTemplate}, enumName, initFuncName, typeName):
|
||||||
func funcName*(
|
func initFuncName*(
|
||||||
value: seq[string],
|
value: seq[string],
|
||||||
altId: Option[string] = none[string](),
|
altId: Option[string] = none[string](),
|
||||||
group: Option[string] = none[string](),
|
group: Option[string] = none[string](),
|
||||||
@ -630,9 +664,9 @@ macro genTextOrUriPropInitializers(
|
|||||||
|
|
||||||
result = newStmtList()
|
result = newStmtList()
|
||||||
for prop in properties:
|
for prop in properties:
|
||||||
let (enumName, funcName, typeName) = namesForProp(prop)
|
let (enumName, typeName, initFuncName, _) = namesForProp(prop)
|
||||||
let funcDef = genAstOpt({kDirtyTemplate}, enumName, funcName, typeName):
|
let funcDef = genAstOpt({kDirtyTemplate}, enumName, initFuncName, typeName):
|
||||||
func funcName*(
|
func initFuncName*(
|
||||||
value: string,
|
value: string,
|
||||||
isUrl = false,
|
isUrl = false,
|
||||||
altId: Option[string] = none[string](),
|
altId: Option[string] = none[string](),
|
||||||
@ -653,9 +687,9 @@ macro genUriPropInitializers(
|
|||||||
|
|
||||||
result = newStmtList()
|
result = newStmtList()
|
||||||
for prop in properties:
|
for prop in properties:
|
||||||
let (enumName, funcName, typeName) = namesForProp(prop)
|
let (enumName, typeName, initFuncName, _) = namesForProp(prop)
|
||||||
let funcDef = genAstOpt({kDirtyTemplate}, enumName, funcName, typeName):
|
let funcDef = genAstOpt({kDirtyTemplate}, enumName, initFuncName, typeName):
|
||||||
func funcName*(
|
func initFuncName*(
|
||||||
value: string,
|
value: string,
|
||||||
altId: Option[string] = none[string](),
|
altId: Option[string] = none[string](),
|
||||||
mediaType: Option[string] = none[string](),
|
mediaType: Option[string] = none[string](),
|
||||||
@ -703,7 +737,7 @@ func newVC4_N*(
|
|||||||
|
|
||||||
func newVC4_Gender*(
|
func newVC4_Gender*(
|
||||||
sex: Option[VC4_Sex] = none[VC4_Sex](),
|
sex: Option[VC4_Sex] = none[VC4_Sex](),
|
||||||
gender: Option[string] = none[string](),
|
genderIdentity: Option[string] = none[string](),
|
||||||
altId: Option[string] = none[string](),
|
altId: Option[string] = none[string](),
|
||||||
group: Option[string] = none[string](),
|
group: Option[string] = none[string](),
|
||||||
params: seq[VCParam] = @[]): VC4_Gender =
|
params: seq[VCParam] = @[]): VC4_Gender =
|
||||||
@ -711,7 +745,7 @@ func newVC4_Gender*(
|
|||||||
return assignFields(
|
return assignFields(
|
||||||
VC4_Gender(params: flattenParameters(params,
|
VC4_Gender(params: flattenParameters(params,
|
||||||
("ALTID", if altId.isSome: @[altId.get] else: @[]))),
|
("ALTID", if altId.isSome: @[altId.get] else: @[]))),
|
||||||
sex, gender, group)
|
sex, genderIdentity, group)
|
||||||
|
|
||||||
func newVC4_Adr*(
|
func newVC4_Adr*(
|
||||||
poBox = "",
|
poBox = "",
|
||||||
@ -769,6 +803,118 @@ func newVC4_Rev*(
|
|||||||
# Accessors
|
# 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 =
|
macro genTypeAccessors(props: static[openarray[VC4_PropertyName]]): untyped =
|
||||||
|
|
||||||
result = newStmtList()
|
result = newStmtList()
|
||||||
@ -785,44 +931,42 @@ macro genTypeAccessors(props: static[openarray[VC4_PropertyName]]): untyped =
|
|||||||
else: return @[]
|
else: return @[]
|
||||||
result.add(typeFun)
|
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 =
|
func altId*(p: VC4_Property): Option[string] =
|
||||||
result = newStmtList()
|
p.params.getSingleValue("ALTID")
|
||||||
|
|
||||||
let genericFunc = genAstOpt({kDirtyTemplate}):
|
func valueType*(p: VC4_Property): Option[string] =
|
||||||
func name*(p: VC4_Property): string =
|
p.params.getSingleValue("VALUE")
|
||||||
if p of VC4_UnknownProperty:
|
|
||||||
return cast[VC4_UnknownProperty](p).name
|
|
||||||
|
|
||||||
let genericIfBlock = genericFunc[6][0]
|
func allAlternatives*[T](vc4: VCard4): Table[string, seq[T]] =
|
||||||
result.add(genericFunc)
|
result = initTable[string, seq[T]]()
|
||||||
|
|
||||||
block:
|
for p in vc4.content:
|
||||||
let funcDef = genAstOpt({kDirtyTemplate}):
|
if p of T:
|
||||||
func name*(p: VC4_UnknownProperty): string = p.name
|
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
|
genPropAccessors(propertyCardMap.pairs.toSeq -->
|
||||||
let propNamesToProcess = (memSafePNs --> filter(it != pnUnknown))
|
filter(not [pnVersion, pnUnknown].contains(it[0])))
|
||||||
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
|
|
||||||
|
|
||||||
genNameAccessors(toSeq(VC4_PropertyName))
|
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
|
# Setters
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@ -851,50 +995,120 @@ func updateOrAdd*[T: VC4_Property](vc4: VCard4, content: seq[T]): VCard4 =
|
|||||||
|
|
||||||
# Ouptut
|
# 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 `$`*(pid: PidValue): string = $pid.propertyId & "." & $pid.sourceId
|
||||||
|
|
||||||
#[
|
|
||||||
func `$`*(vc4: VCard4): string =
|
func `$`*(vc4: VCard4): string =
|
||||||
result = "BEGIN:VCARD" & CRLF
|
result = "BEGIN:VCARD" & CRLF
|
||||||
result &= "VERSION:4.0" & CRLF
|
result &= "VERSION:4.0" & CRLF
|
||||||
for p in (vc4.content --> filter(not (it of VC4_Version))):
|
for p in (vc4.content --> filter(not (it of VC4_Version))):
|
||||||
result &= foldContentLine(serialize(p)) & CRLF
|
result &= foldContentLine(serialize(p)) & CRLF
|
||||||
result &= "END:VCARD" & CRLF
|
result &= "END:VCARD" & CRLF
|
||||||
]#
|
|
||||||
|
|
||||||
|
|
||||||
# Parsing
|
# 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 =
|
proc readParamValue(p: var VCardParser): string =
|
||||||
## Read a single parameter value at the current read position or error. Note
|
## Read a single parameter value at the current read position or error. Note
|
||||||
## that this implementation differs from RFC 6450 in two important ways:
|
## 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 " &
|
p.error("quoted parameter value expected to end with a " &
|
||||||
"double quote (\")")
|
"double quote (\")")
|
||||||
|
|
||||||
if result.len == 0:
|
|
||||||
p.error("expected to read a parameter value")
|
|
||||||
|
|
||||||
proc readParams(p: var VCardParser): seq[VCParam] =
|
proc readParams(p: var VCardParser): seq[VCParam] =
|
||||||
result = @[]
|
result = @[]
|
||||||
while p.peek == ';':
|
while p.peek == ';':
|
||||||
@ -1030,7 +1241,7 @@ proc readTextValueList(
|
|||||||
while seps.contains(p.peek): result.add(p.readTextValue(ignorePrefix = seps))
|
while seps.contains(p.peek): result.add(p.readTextValue(ignorePrefix = seps))
|
||||||
|
|
||||||
macro genPropParsers(
|
macro genPropParsers(
|
||||||
genProps: static[openarray[tuple[a: VC4_PropertyName, b: VC4_ValueType]]],
|
genProps: static[openarray[(VC4_PropertyName, VC4_ValueType)]],
|
||||||
group: Option[string],
|
group: Option[string],
|
||||||
name: string,
|
name: string,
|
||||||
params: seq[VCParam],
|
params: seq[VCParam],
|
||||||
@ -1049,7 +1260,7 @@ macro genPropParsers(
|
|||||||
parseEnum[VC4_PropertyName](name, pnUnknown))
|
parseEnum[VC4_PropertyName](name, pnUnknown))
|
||||||
|
|
||||||
for (pn, pt) in genProps:
|
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())
|
let parseCase = nnkOfBranch.newTree(quote do: `enumName`, newEmptyNode())
|
||||||
result.add(parseCase)
|
result.add(parseCase)
|
||||||
@ -1057,7 +1268,7 @@ macro genPropParsers(
|
|||||||
case pt
|
case pt
|
||||||
of vtDateTimeOrText:
|
of vtDateTimeOrText:
|
||||||
parseCase[1] = genAst(contents, initFuncName, typeName, p):
|
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
|
if valueType.isSome and valueType.get != $vtDateAndOrTime and
|
||||||
valueType.get != $vtText:
|
valueType.get != $vtText:
|
||||||
p.error("VALUE must be either \"date-and-or-time\" or \"text\" for " &
|
p.error("VALUE must be either \"date-and-or-time\" or \"text\" for " &
|
||||||
@ -1065,11 +1276,12 @@ macro genPropParsers(
|
|||||||
|
|
||||||
contents.add(initFuncName(
|
contents.add(initFuncName(
|
||||||
value = p.readValue,
|
value = p.readValue,
|
||||||
|
valueType = valueType,
|
||||||
group = group,
|
group = group,
|
||||||
params = params))
|
params = params))
|
||||||
|
|
||||||
of vtText:
|
of vtText:
|
||||||
parseCase[1] = genAst(contents, typeName):
|
parseCase[1] = genAst(contents, typeName, pt):
|
||||||
p.validateType(params, pt)
|
p.validateType(params, pt)
|
||||||
contents.add(ac(typeName(value: p.readTextValue)))
|
contents.add(ac(typeName(value: p.readTextValue)))
|
||||||
|
|
||||||
@ -1080,17 +1292,17 @@ macro genPropParsers(
|
|||||||
|
|
||||||
of vtTextOrUri:
|
of vtTextOrUri:
|
||||||
parseCase[1] = genAst(contents, typeName):
|
parseCase[1] = genAst(contents, typeName):
|
||||||
let valueType = params.getSingleValue("TYPE")
|
let valueType = params.getSingleValue("VALUE")
|
||||||
if valueType.isNone or valueType.get == $vtUri:
|
if valueType.isNone or valueType.get == $vtUri:
|
||||||
contents.add(ac(typeName(value: p.readValue)))
|
contents.add(ac(typeName(value: p.readValue)))
|
||||||
elif valueType.isSome and valueType.get == $vtText:
|
elif valueType.isSome and valueType.get == $vtText:
|
||||||
contents.add(ac(typeName(value: p.readTextValue)))
|
contents.add(ac(typeName(value: p.readTextValue)))
|
||||||
else:
|
else:
|
||||||
p.error("VALUE must be either \"text\" or \"uri\" for " & name &
|
p.error(("VALUE must be either \"text\" or \"uri\" for $# " &
|
||||||
" properties.")
|
"properties (was $#).") % [name, $valueType])
|
||||||
|
|
||||||
of vtUri:
|
of vtUri:
|
||||||
parseCase[1] = genAst(typeName, contents):
|
parseCase[1] = genAst(typeName, contents, pt):
|
||||||
p.validateType(params, pt)
|
p.validateType(params, pt)
|
||||||
contents.add(ac(typeName(value: p.readValue)))
|
contents.add(ac(typeName(value: p.readValue)))
|
||||||
|
|
||||||
@ -1126,11 +1338,11 @@ macro genPropParsers(
|
|||||||
|
|
||||||
contents.add(ac(VC4_Gender(
|
contents.add(ac(VC4_Gender(
|
||||||
sex: sex,
|
sex: sex,
|
||||||
gender:
|
genderIdentity:
|
||||||
if sexCh == ';' or sex.isSome: some(p.readTextValue)
|
if sexCh == ';' or p.peek == ';':
|
||||||
|
some(p.readTextValue(ignorePrefix = {';'}))
|
||||||
else: none[string]())))
|
else: none[string]())))
|
||||||
|
|
||||||
|
|
||||||
block: # ADR
|
block: # ADR
|
||||||
let parseCase = nnkOfBranch.newTree(ident("pnAdr"), newEmptyNode())
|
let parseCase = nnkOfBranch.newTree(ident("pnAdr"), newEmptyNode())
|
||||||
result.add(parseCase)
|
result.add(parseCase)
|
||||||
@ -1164,7 +1376,7 @@ macro genPropParsers(
|
|||||||
let parseCase = nnkOfBranch.newTree(ident("pnUnknown"), newEmptyNode())
|
let parseCase = nnkOfBranch.newTree(ident("pnUnknown"), newEmptyNode())
|
||||||
result.add(parseCase)
|
result.add(parseCase)
|
||||||
parseCase[1] = genAst(contents):
|
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
|
# echo result.repr
|
||||||
|
|
||||||
proc parseContentLines*(p: var VCardParser): seq[VC4_Property] =
|
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)
|
genPropParsers(fixedValueTypeProperties, group, name, params, result, p)
|
||||||
|
|
||||||
#[
|
p.expect(CRLF)
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
# Private Function Unit Tests
|
# Private Function Unit Tests
|
||||||
|
1
tests/allen.foster.jpg.uri
Normal file
1
tests/allen.foster.jpg.uri
Normal file
File diff suppressed because one or more lines are too long
212
tests/allen.foster.v4.vcf
Normal file
212
tests/allen.foster.v4.vcf
Normal 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
|
||||||
|
AAAAUgAAABwARwBJAE0AUAAgAGIAdQBpAGwAdAAtAGkAbgAgAEQANgA1ACAARwByAGEAeQBzAGMAYQ
|
||||||
|
BsAGUAIAB3AGkAdABoACAAcwBSAEcAQgAgAFQAUgBDAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAABoA
|
||||||
|
AAAcAFAAdQBiAGwAaQBjACAARABvAG0AYQBpAG4AAFhZWiAAAAAAAADzUQABAAAAARbMcGFyYQAAAA
|
||||||
|
AAAwAAAAJmZgAA8qcAAA1ZAAAT0AAAClttbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAEcASQBN
|
||||||
|
AFBtbHVjAAAAAAAAAAEAAAAMZW5VUwAAADYAAAAcAEQANgA1ACAARwByAGEAeQBzAGMAYQBsAGUAIA
|
||||||
|
B3AGkAdABoACAAcwBSAEcAQgAgAFQAUgBDAAD/2wBDAAoHBwgHBgoICAgLCgoLDhgQDg0NDh0VFhEY
|
||||||
|
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
|
||||||
|
GAaDKL5DfxBTTUUbUr1Zb/APP/AP/Z
|
||||||
|
END:VCARD
|
@ -1,4 +1,5 @@
|
|||||||
import options, unittest, zero_functional
|
import std/[options, strutils, tables, unittest]
|
||||||
|
import zero_functional
|
||||||
|
|
||||||
import ./vcard
|
import ./vcard
|
||||||
import ./vcard/vcard4
|
import ./vcard/vcard4
|
||||||
@ -7,3 +8,243 @@ suite "vcard/vcard4":
|
|||||||
|
|
||||||
test "vcard4/private tests":
|
test "vcard4/private tests":
|
||||||
runVcard4PrivateTests()
|
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."]]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user