diff --git a/src/vcard/vcard4.nim b/src/vcard/vcard4.nim index eea6d39..b61d63a 100644 --- a/src/vcard/vcard4.nim +++ b/src/vcard/vcard4.nim @@ -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,7 +605,8 @@ macro genDateTimeOrTextPropInitializers( value: value, valueType: vtText) - result.parseDateAndOrTime(value) + if valueType.isNone or valueType.get == $vtDateAndOrTime: + result.parseDateAndOrTime(value) addConditionalParams(prop, datetimeFuncDef) addConditionalParams(prop, textFuncDef) @@ -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 diff --git a/tests/allen.foster.jpg.uri b/tests/allen.foster.jpg.uri new file mode 100644 index 0000000..9bdbfed --- /dev/null +++ b/tests/allen.foster.jpg.uri @@ -0,0 +1 @@ +data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEFeAV4AAD/4QCuRXhpZgAASUkqAAgAAAAHABIBAwABAAAAAQAAABoBBQABAAAAYgAAABsBBQABAAAAagAAACgBAwABAAAAAgAAADEBAgANAAAAcgAAADIBAgAUAAAAgAAAAGmHBAABAAAAlAAAAAAAAAB4BQAAAQAAAHgFAAABAAAAR0lNUCAyLjEwLjM0AAAyMDIzOjA0OjI1IDE2OjQzOjUyAAEAAaADAAEAAAABAAAAAAAAAP/hDM1odHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDQuNC4wLUV4aXYyIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6R0lNUD0iaHR0cDovL3d3dy5naW1wLm9yZy94bXAvIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9ImdpbXA6ZG9jaWQ6Z2ltcDpmYzVkZDFkMC05ZmNiLTRhZjAtOGUzNS1jMTMyMDU4NTUwMmEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6YmEyOThkOGYtMDY3NC00ZDgzLWJhZGMtNWVkY2Y2OTg2NTBjIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6NzJjYWViYjMtMjkzMy00ZGJmLTg0M2EtYzYwYjBkZWYzMzdlIiBkYzpGb3JtYXQ9ImltYWdlL2pwZWciIEdJTVA6QVBJPSIyLjAiIEdJTVA6UGxhdGZvcm09IldpbmRvd3MiIEdJTVA6VGltZVN0YW1wPSIxNjgyNDU5MDQ2MjA4NjE3IiBHSU1QOlZlcnNpb249IjIuMTAuMzQiIHhtcDpDcmVhdG9yVG9vbD0iR0lNUCAyLjEwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIzOjA0OjI1VDE2OjQzOjUyLTA1OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMzowNDoyNVQxNjo0Mzo1Mi0wNTowMCI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDpjaGFuZ2VkPSIvIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjU2YzcxYmUyLWExNmMtNDE2OC1iNDA5LWI3YjRlMTgwZTFmMyIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iR2ltcCAyLjEwIChXaW5kb3dzKSIgc3RFdnQ6d2hlbj0iMjAyMy0wNC0yNVQxNjo0NDowNiIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPD94cGFja2V0IGVuZD0idyI/Pv/iAjBJQ0NfUFJPRklMRQABAQAAAiBsY21zBEAAAG1udHJHUkFZWFlaIAfnAAQAGQAVACoAJWFjc3BNU0ZUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD21gABAAAAANMtbGNtcwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA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 \ No newline at end of file diff --git a/tests/allen.foster.v4.vcf b/tests/allen.foster.v4.vcf new file mode 100644 index 0000000..14fc4c3 --- /dev/null +++ b/tests/allen.foster.v4.vcf @@ -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 + 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 + GAaDKL5DfxBTTUUbUr1Zb/APP/AP/Z +END:VCARD diff --git a/tests/tvcard4.nim b/tests/tvcard4.nim index 1a4093c..12464a2 100644 --- a/tests/tvcard4.nim +++ b/tests/tvcard4.nim @@ -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."]]