From 483717d212cd5463b7b9740e8b8df0a2d13e3b4f Mon Sep 17 00:00:00 2001 From: nitely Date: Fri, 31 Jul 2026 14:31:26 -0300 Subject: [PATCH] CDDL printer --- cbor_serialization.nimble | 3 +- cbor_serialization/cddl/parser.nim | 6 +- cbor_serialization/cddl/parser2.nim | 1319 +++++++++++ cbor_serialization/cddl/parser2_schema.md | 121 + cbor_serialization/cddl/type_generator.nim | 165 +- tests/test_all.nim | 6 +- tests/test_cddl_gen.nim | 4 +- tests/test_cddl_parser.nim | 774 ++++++- tests/test_cddl_parser_36_dump.txt | 248 -- tests/test_cddl_parser_36_dump_pretty.txt | 142 ++ tests/test_cddl_parser_dump.txt | 2389 ++++++++++++++++---- tests/test_cddl_parser_dump_pretty.txt | 685 ++++++ tests/test_cddl_parser_old.nim | 29 + tests/test_writer.nim | 4 + 14 files changed, 5087 insertions(+), 808 deletions(-) create mode 100644 cbor_serialization/cddl/parser2.nim create mode 100644 cbor_serialization/cddl/parser2_schema.md delete mode 100644 tests/test_cddl_parser_36_dump.txt create mode 100644 tests/test_cddl_parser_36_dump_pretty.txt create mode 100644 tests/test_cddl_parser_dump_pretty.txt create mode 100644 tests/test_cddl_parser_old.nim diff --git a/cbor_serialization.nimble b/cbor_serialization.nimble index fba51ec..f23b5ab 100644 --- a/cbor_serialization.nimble +++ b/cbor_serialization.nimble @@ -17,7 +17,8 @@ license = "Apache License 2.0" skipDirs = @["tests", "fuzzer"] requires "nim >= 2.0.0", - "serialization >= 0.4.9", "stew >= 0.4.1", "npeg >= 1.3.0", "results", "unittest2" + "faststreams >= 0.5.1", "serialization >= 0.4.9", "stew >= 0.4.1", "npeg >= 1.3.0", + "results", "unittest2" #feature "dev": # requires "bigints", "json_serialization" diff --git a/cbor_serialization/cddl/parser.nim b/cbor_serialization/cddl/parser.nim index 8cc999a..acd6c02 100644 --- a/cbor_serialization/cddl/parser.nim +++ b/cbor_serialization/cddl/parser.nim @@ -9,6 +9,8 @@ #{.push raises: [], gcsafe.} +{.deprecated: "use parser2".} + import std/strutils import npeg, results @@ -120,7 +122,9 @@ proc newCddlError(s: string, matchLen, matchMax: int): ref CborCddlError = # https://datatracker.ietf.org/doc/html/rfc8610#appendix-A # https://datatracker.ietf.org/doc/html/rfc8610#appendix-B # https://github.com/zevv/npeg#ordering-of-rules-in-a-grammar -proc parseCddl*(source: string): CddlSchema {.raises: [CborCddlError].} = +proc parseCddl*( + source: string +): CddlSchema {.raises: [CborCddlError], deprecated: "use parser2.parseCddl".} = let parser = peg("cddl", userdata: ParseState): cddl <- S * +(rule * S) * !1 diff --git a/cbor_serialization/cddl/parser2.nim b/cbor_serialization/cddl/parser2.nim new file mode 100644 index 0000000..6d78d30 --- /dev/null +++ b/cbor_serialization/cddl/parser2.nim @@ -0,0 +1,1319 @@ +# cbor-serialization +# Copyright (c) 2026 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +{.push raises: [], gcsafe.} + +import std/strutils +import npeg, results +import faststreams/[outputs, textio] + +export results + +type + CddlOccurKind* {.pure.} = enum + ocOne ## no occurrence marker -> exactly 1 + ocOptional ## "?" + ocOneOrMore ## "+" + ocZeroOrMore ## "*" + ocRange ## n*m (lo / hi); hi may be absent + + CddlOccur* = object + kind*: CddlOccurKind + lo*: Opt[uint64] + hi*: Opt[uint64] + + CddlSepKind* {.pure.} = enum + ## What separates a member's key from its type + skNone ## positional (no memberkey) + skColon ## name: type "k": type 1: type + skArrow ## type1 S "=>" type + skArrowCut ## type1 S "^" S "=>" type + + CddlRuleKind* {.pure.} = enum + rkType ## typename = type + rkTypeExt ## typename /= type (extends a type choice) + rkGroup ## groupname = grpent + rkGroupExt ## groupname //= grpent (extends a group choice) + + CddlRangeKind* {.pure.} = enum + rngInclusive ## ".." upper bound is part of the range + rngExclusive ## "..." upper bound is excluded from the range + + CddlNodeKind* {.pure.} = enum + ## The children each kind carries are listed after the syntax it stands for + nkUnset ## nothing; children: - + nkSchema ## a whole schema; children: rules + nkRule ## name ? assign body; children: params, body + nkEntry ## ?occur ?memberkey type; children: ?key, type + nkParam ## the a and b a rule declares in "id = ..."; children: - + nkTypeRef ## tstr, uint, MyType; children: - + nkValue ## 0, "ok", h'0a'; children: - + nkMap ## { ... }; children: members + nkArray ## [ ... ]; children: members + nkGroup ## ( ... ); children: members + nkGroupChoice ## grpchoice // grpchoice; children: alternatives, nkGroup + nkUnion ## type1 / type2 / ...; children: variants + nkTagged ## #6.N( ... ); children: type + nkMajor ## #N #N.M; children: - + nkAny ## bare #; children: - + nkGeneric ## id<...>; children: arguments + nkRange ## 0..10 0...10; children: lhs, rhs + nkControl ## bstr .size 16; children: lhs, rhs + nkParen ## ( type ); children: type + nkUnwrap ## ~id ~id<...>; children: type + nkEnum ## &id &id<...> &( ... ); children: type + nkComment ## ; a comment, on a line of its own; children: - + nkCommentInline ## ; a comment, after what was written before it; children: - + nkEmpty ## used for top-level blank lines children: - + + CddlNodeVal* = object + ## CddlNode value + + # nkRule: + ruleText*: string + ruleKind*: CddlRuleKind + # nkEntry: + occur*: CddlOccur + sepKind*: CddlSepKind + # nkRange: + rangeKind*: CddlRangeKind + # nkTagged: + major*: uint8 + minor*: Opt[uint64] + # else: + text*: string + + CddlNode* = object + # The members of a map, array or group are nkEntry's, except when the + # group is a choice, in which case there is a single nkGroupChoice child + kind*: CddlNodeKind + children*: seq[CddlNode] + val*: CddlNodeVal + + CddlSchema* = CddlNode + ## An nkSchema node, whose children are the nkRule's of a schema in source + ## order, and the comments and blank lines written among them. A container + ## like any other, so that everything reading or writing one works the same + ## way here. + + CborCddlError* = object of CatchableError + + Nl = object ## Blank line or comment + si: int ## where it starts in the source + kind: CddlNodeKind ## one of nlKinds + text: string ## from its ';' up to, but not including, its newline + + ParseState = object + schema: seq[CddlNode] ## the children the nkSchema is built from at the end + + # A copy of what is being parsed, to tell a comment at the end of a line + # from one written on a line of its own, and a blank line from a line that + # has something on it + source: string + + # Read comments and blank lines as the whitespace they stand in for, + # keeping none + ignoreComments: bool + + # NLs read but not handed to a node yet, in source order + pending: seq[Nl] + + ruleName: string + ruleParams: seq[CddlNode] + ruleKind: CddlRuleKind + + # The entry being built + wip: CddlNode + + # Open containers, pushed on '{' '[' '(' '<' and popped on the closing + # delimiter; each holds the entry that owns the container + nested: seq[CddlNode] + + # The variants of every type choice being parsed, innermost last, so that + # a nested choice, ie the "(2 / 3)" in "1 / (2 / 3)", keeps its own + variantStack: seq[seq[CddlNode]] + + # Operands of a pending rangeop/ctlop expression; a type1 pushes its + # operand and pops the (possibly combined) result, so nested type1's, + # ie the "1..32" in "tstr .size (1..32)", don't clobber the outer one + operandStack: seq[CddlNode] + + # Why an action could not make sense of what it matched, read once the + # match is over; see parseNum for why this is not raised on the spot + error: string + +# Needed to avoid expensive AST copies; +# if it's too hard to work with in practice +# make CddlNode a ref and drop const support +when (NimMajor, NimMinor) >= (2, 2) and defined(gcOrc): + proc `=copy`(dst: var CddlNode, src: CddlNode) {.error.} + +const + typeRules* = {rkType, rkTypeExt} ## rules binding a type + groupRules* = {rkGroup, rkGroupExt} ## rules binding a group entry + commentKinds* = {nkComment, nkCommentInline} + ## nodes carrying a comment rather than anything a schema is made of + nlKinds* = commentKinds + {nkEmpty} ## blank line and comments + typeKinds* = { + nkTypeRef, nkValue, nkMap, nkArray, nkGroup, nkUnion, nkTagged, nkMajor, nkAny, + nkGeneric, nkRange, nkControl, nkParen, nkUnwrap, nkEnum, + } ## the kinds that can stand where a type is called for + +# XXX https://github.com/nim-lang/Nim/issues/26081 +func `==`*(a, b: CddlNode): bool = + a.kind == b.kind and a.val == b.val and a.children == b.children + +func body*(n: CddlNode): lent CddlNode = + ## Type a rule binds, or an entry is an occurrence of; the last child either + ## way, since a rule may have parameters and an entry a key in front of it. + doAssert n.kind in {nkRule, nkEntry} + n.children[n.children.high] # "^1" would not be addressable, so not lent + +template genericParams*(n: CddlNode): untyped = + ## The nkParam's an nkRule declares, empty when it is not generic. A view of + ## the children before the body, so that reading them copies nothing; a + ## template, since a func cannot hand an openArray back out of the node. + n.children.toOpenArray(0, n.children.high - 1) + +func key*(n: CddlNode): lent CddlNode = + ## Member key of an entry. Under skColon an nkValue is the literal it was + ## written as, and an nkTypeRef the bareword RFC 8610 defines as the text + ## string of that name; under the arrows, the type the key was written as. + doAssert n.kind == nkEntry and n.val.sepKind != skNone + n.children[0] + +func hasCut*(n: CddlNode): bool = + ## Whether a match on the key of this entry commits to it, so that a later + ## entry cannot match the same key. RFC 8610 3.5.4 makes "k: t" a shorthand + ## for "k ^ => t", so the colon form cuts as well. + doAssert n.kind == nkEntry + n.val.sepKind in {skColon, skArrowCut} + +iterator entries*(n: CddlNode): lent CddlNode = + ## Members of a map, array or group, without the comments and blank + ## lines written among them. Everything walking a container wants this rather than children. + for i in 0 ..< n.children.len: + if n.children[i].kind notin nlKinds: + yield n.children[i] + +func entryCount*(n: CddlNode): int = + ## How many members a map, array or group has, an NL not counted. + for i in 0 ..< n.children.len: + if n.children[i].kind notin nlKinds: + inc result + +func firstEntry*(n: CddlNode): lent CddlNode = + ## The first member of a map, array or group that is not an NL. + for i in 0 ..< n.children.len: + if n.children[i].kind notin nlKinds: + return n.children[i] + raiseAssert "container has no entries" + +func target*(n: CddlNode): lent CddlNode = + ## Type a tag, a paren or the "~" / "&" operators are applied to; for + ## "&( ... )" this is the nkGroup itself, otherwise the referenced name as + ## nkTypeRef/nkGeneric. + doAssert n.kind in {nkTagged, nkParen, nkUnwrap, nkEnum} + n.children[0] + +func unparen*(n: CddlNode): lent CddlNode = + ## The type inside any parens it was written in. The grammar allows them + ## around any type, where they carry no meaning of their own. + result = n + while result.kind == nkParen: + result = result.children[0] + +func lhs*(n: CddlNode): lent CddlNode = + ## Lower bound of a range, or the type a control operator applies to. + doAssert n.kind in {nkRange, nkControl} + n.children[0] + +func rhs*(n: CddlNode): lent CddlNode = + ## Upper bound of a range, included only when rangeKind is rngInclusive, or + ## the controller type of a control operator. + doAssert n.kind in {nkRange, nkControl} + n.children[1] + +func assignText*(rk: CddlRuleKind): string = + ## The "=", "/=" or "//=" a rule of this kind is written with, spaced. + case rk + of rkType: " = " + of rkTypeExt: " /= " + of rkGroup: " = " + of rkGroupExt: " //= " + +func rangeOpText*(rk: CddlRangeKind): string = + case rk + of rngInclusive: ".." + of rngExclusive: "..." + +func parseNum(s: string, err: var string, what: string): Opt[uint64] = + var v = 0'u64 + for c in s: + if c notin {'0' .. '9'}: # the grammar also admits "0x" and "0b" numbers + err = what & " is not a decimal number: " & s + return Opt.none(uint64) + let digit = uint64(ord(c) - ord('0')) + if v > (uint64.high - digit) div 10: + err = what & " out of range: " & s + return Opt.none(uint64) + v = v * 10 + digit + Opt.some(v) + +func majorVal(s: string, err: var string): CddlNodeVal = + ## The "6.32" or "3" a '#' was followed by, as the major type and the number + ## after its '.'. The leading digit is one by the grammar. + result = CddlNodeVal(major: uint8(ord(s[0]) - ord('0'))) + let dot = s.find('.') + if dot >= 0: + result.minor = parseNum(s[dot + 1 .. ^1], err, "tag number") + +func initEntry(): CddlNode = + ## An entry with an empty type slot, ready to be filled in by the parser. + CddlNode(kind: nkEntry, children: @[CddlNode()]) + +template typeSlot(n: untyped): untyped = + ## Where an entry's type is read into: its last child, so that a member key + ## can be put in front of it once one is read. Indexed from the front: "^1" + ## goes through a proc that hands back a copy of the whole subtree. + n.children[n.children.high] + +template `typeSlot=`(n, typ: untyped) = + # a setter of its own: "n.typeSlot = x" is resolved as one before templates + n.children[n.children.high] = typ + +template members(state: untyped): untyped = + ## The members read so far into the container that is currently open. + state.nested[state.nested.high].typeSlot.children + +template `members=`(state, nodes: untyped) = + state.nested[state.nested.high].typeSlot.children = nodes + +# XXX fails for refc +#proc namedType(state: var ParseState, name: string): CddlNode = +# if state.wip.typeSlot.children.len > 0: +# CddlNode(kind: nkGeneric, val: CddlNodeVal(text: name), children: move(state.wip.typeSlot.children)) +# else: +# CddlNode(kind: nkTypeRef, val: CddlNodeVal(text: name)) + +func namedType(name: string, args: sink seq[CddlNode]): CddlNode = + if args.len > 0: + CddlNode(kind: nkGeneric, val: CddlNodeVal(text: name), children: args) + else: + CddlNode(kind: nkTypeRef, val: CddlNodeVal(text: name)) + +func isOwnLine(source: string, si: int): bool = + ## Whether the comment starting here is the first thing written on its line. + var i = si - 1 + while i >= 0 and source[i] in {' ', '\t', '\r'}: + dec i + i < 0 or source[i] == '\n' + +proc dropPendingFrom(state: var ParseState, si: int) = + ## Forget the NLs read from here on. A branch the parser abandons leaves + ## behind whatever it read, so it goes before anything is read a second time: + ## the branch that replaces it reads its own NLs again, at the same + ## offsets. + while state.pending.len > 0 and state.pending[^1].si >= si: + discard state.pending.pop() + +proc addComment(state: var ParseState, text: string, si: int) = + ## Hold on to a comment until a rule or an entry takes it. + if state.ignoreComments: + return + state.dropPendingFrom(si) + state.pending.add Nl( + si: si, + kind: if isOwnLine(state.source, si): nkComment else: nkCommentInline, + text: text, + ) + +proc addBlank(state: var ParseState, si: int) = + ## Add blank line; fold multiple blank lines into one + if state.ignoreComments or state.nested.len > 0 or not isOwnLine(state.source, si): + return + state.dropPendingFrom(si) + if state.pending.len > 0 and state.pending[^1].kind == nkEmpty: + return + state.pending.add Nl(si: si, kind: nkEmpty) + +proc flushNl(state: var ParseState, dest: var seq[CddlNode], blanks = false) = + ## Write the NLs read so far into dest. + for i in 0 ..< state.pending.len: + if blanks or state.pending[i].kind != nkEmpty: + dest.add CddlNode( + kind: state.pending[i].kind, val: CddlNodeVal(text: state.pending[i].text) + ) + state.pending.setLen 0 + +proc openContainer(state: var ParseState) = + ## Start reading the members of a container into the type slot of the entry + ## that owns it - the entry has to be kept because parsing those members + ## overwrites it - and begin a fresh entry for the first member. + reset(state.wip.typeSlot) + state.nested.add move(state.wip) # finished with; hand it over, don't copy + state.wip = initEntry() + state.flushNl(state.members) + +proc closeContainer(state: var ParseState, kind: CddlNodeKind) = + ## Give the container its kind and the entry that owns it back. + # anything read after the last member was written inside here, and there is + # no way back to these members once the container is popped + state.flushNl(state.members) + state.wip = state.nested.pop() + state.wip.typeSlot.kind = kind + +proc newCddlError(s: string, matchLen, matchMax: int): ref CborCddlError = + let posA = max(0, min(s.high, matchLen)) + let posB = max(-1, min(s.high, matchMax)) + let lineA = s[0 ..< posA].count('\n') + 1 + let lineB = s[0 ..< max(0, posB)].count('\n') + 1 + let line = + if lineA != lineB: + $lineA & "-" & $lineB + else: + $lineA + (ref CborCddlError)( + msg: "CBOR CDDL failed to parse line " & line & ": " & s[posA .. posB] + ) + +{.pop.} # {.push raises: [], gcsafe.} + +# https://datatracker.ietf.org/doc/html/rfc8610#appendix-A +# https://datatracker.ietf.org/doc/html/rfc8610#appendix-B +# https://github.com/zevv/npeg#ordering-of-rules-in-a-grammar +proc parseCddl*( + source: string, ignoreComments = false +): CddlSchema {.raises: [CborCddlError], gcsafe.} = + ## Parse a CDDL schema into its rules. With ignoreComments a comment, and the + ## blank lines a schema is laid out with, are read as the whitespace they + ## stand in for and nothing is kept of them, so that what comes back is rules + ## and their members and nothing else; leave it off to keep the nkComment, + ## nkCommentInline and nkEmpty nodes written among them. + let parser = peg("cddl", userdata: ParseState): + cddl <- S * +(rule * S) * !1 + + #rule <- (typename * ?genericparm * S * assignt * S * typ) | + # (groupname * ?genericparm * S * assigng * S * grpent) + rule <- ( + (ruleTypename * ?genericparm * S * ruleAssignt * S * typ) | + (ruleTypename * ?genericparm * S * ruleAssigng * S * grpent) + ) do: + # the body goes after the parameters read so far, as the last child + userdata.ruleParams.add( + if userdata.ruleKind in groupRules: + move(userdata.wip) + else: + move(userdata.wip.typeSlot) + ) + userdata.schema.add CddlNode( + kind: nkRule, + val: CddlNodeVal(ruleText: userdata.ruleName, ruleKind: userdata.ruleKind), + children: move(userdata.ruleParams), + ) + userdata.wip = initEntry() + + ruleTypename <- >id do: + userdata.ruleName = $1 + userdata.ruleParams.setLen 0 + userdata.flushNl(userdata.schema, blanks = true) + + # assigng <- '=' | "//=" + ruleAssigng <- >('=' | "//=") do: + userdata.ruleKind = if $1 == "//=": rkGroupExt else: rkGroup + + # assignt <- '=' | "/=" + ruleAssignt <- >('=' | "/=") do: + userdata.ruleKind = if $1 == "/=": rkTypeExt else: rkType + + # group <- grpchoice * *(S * "//" * S * grpchoice) + group <- grpchoice * *(S * "//" * S * grpchoice) do: + var alts = move(userdata.members) + userdata.members = + if alts.len == 1: + move(alts[0].children) + else: + @[CddlNode(kind: nkGroupChoice, children: move(alts))] + + grpchoice <- *(grpent * optcom) do: + # the members read since the last "//" - the ones not made an + # alternative yet - are the alternative this choice stands for + var first = userdata.members.len + while first > 0 and userdata.members[first - 1].kind != nkGroup: + dec first + var alt = CddlNode(kind: nkGroup) + for i in first ..< userdata.members.len: + alt.children.add move(userdata.members[i]) + userdata.members.setLen first + userdata.members.add move(alt) + + optcom <- S * ?(',' * S) do: + if userdata.nested.len > 0: + userdata.flushNl(userdata.members) + + # grpent <- (?(occur * S) * ?(memberkey * S) * typ) | + # (?(occur * S) * groupname * ?genericarg) | ; preempted by above + # (?(occur * S) * '(' * S * group * S * ')') + grpent <- + ((?(occur * S) * ?(memberkey * S) * typ) | (?(occur * S) * grpentInlineGroup)) do: + if userdata.nested.len > 0: + userdata.members.add move(userdata.wip) + userdata.wip = initEntry() + + grpentInlineGroup <- grpentInlineGroupPush * group * S * ')' do: + userdata.closeContainer(nkGroup) + + grpentInlineGroupPush <- '(' * S do: + userdata.openContainer() + + # memberkey <- (type1 * S * ?('^' * S) * "=>") | + # (bareword * S * ':') | + # (value * S * ':') + memberkey <- memberkeyType | memberkeyName | memberkeyValue + + memberkeyType <- >type1 * S * >?('^' * S) * "=>" do: + userdata.wip.val.sepKind = if ($2).len > 0: skArrowCut else: skArrow + let readKey = move(userdata.wip.typeSlot) + userdata.wip.children = @[readKey, CddlNode()] + + memberkeyName <- >bareword * S * ':' do: + userdata.wip.val.sepKind = skColon + userdata.wip.children = + @[CddlNode(kind: nkTypeRef, val: CddlNodeVal(text: $1)), CddlNode()] + + memberkeyValue <- >value * S * ':' do: + userdata.wip.val.sepKind = skColon + userdata.wip.children = + @[CddlNode(kind: nkValue, val: CddlNodeVal(text: $1)), CddlNode()] + + # typ <- type1 * *(S * '/' * S * type1) + typ <- typFirstVariant * *(S * '/' * S * typVariant) do: + var variants = userdata.variantStack.pop() + userdata.wip.typeSlot = + if variants.len > 1: + CddlNode(kind: nkUnion, children: move(variants)) + else: + move(variants[0]) + + typFirstVariant <- type1 do: + userdata.variantStack.add @[move(userdata.wip.typeSlot)] + + typVariant <- type1 do: + userdata.variantStack[userdata.variantStack.high].add move(userdata.wip.typeSlot) + + # type1 <- type2 * ?(S * (rangeop | ctlop) * S * type2) + type1 <- type1Operand * ?type1WithOp do: + userdata.wip.typeSlot = userdata.operandStack.pop() + + type1WithOp <- S * >(rangeop | ctlop) * S * type1Operand do: + let rhs = userdata.operandStack.pop() + let lhs = userdata.operandStack.pop() + let op = $1 + userdata.operandStack.add( + if op == ".." or op == "...": + CddlNode( + kind: nkRange, + val: + CddlNodeVal(rangeKind: (if op == "...": rngExclusive else: rngInclusive)), + children: @[lhs, rhs], + ) + else: + CddlNode(kind: nkControl, val: CddlNodeVal(text: op), children: @[lhs, rhs]) + ) + + type1Operand <- type2 do: + userdata.operandStack.add move(userdata.wip.typeSlot) + + # type2 <- value | + # (typename * ?genericarg) | + # ('(' * S * typ * S * ')') | + # ('{' * S * group * S * '}') | + # ('[' * S * group * S * ']') | + # ('~' * S * typename * ?genericarg) | + # ('&' * S * '(' * S * group * S * ')') | + # ('&' * S * groupname * ?genericarg) | + # ('#' * '6' * ?('.' * uintx) * '(' * S * typ * S * ')') | + # ('#' * DIGIT * ?('.' * uintx)) | + # '#' + type2 <- + type2Value | type2TypeName | type2Paren | type2Map | type2Array | type2Unwrap | + type2GroupEnum | type2GroupName | type2Tag | type2Major | type2Any + + type2Value <- >value do: + userdata.wip.typeSlot = CddlNode(kind: nkValue, val: CddlNodeVal(text: $1)) + + # (typename * ?genericarg) + type2TypeName <- >refName * ?genericarg do: + userdata.wip.typeSlot = namedType($1, move(userdata.wip.typeSlot.children)) + + # ('(' * S * typ * S * ')') + type2Paren <- '(' * S * typ * S * ')' do: + let inner = move(userdata.wip.typeSlot) + userdata.wip.typeSlot = CddlNode(kind: nkParen, children: @[inner]) + + # ('{' * S * group * S * '}') + type2Map <- type2MapPush * group * S * '}' do: + userdata.closeContainer(nkMap) + + type2MapPush <- '{' * S do: + userdata.openContainer() + + # ('[' * S * group * S * ']') + type2Array <- type2ArrayPush * group * S * ']' do: + userdata.closeContainer(nkArray) + + type2ArrayPush <- '[' * S do: + userdata.openContainer() + + # ('~' * S * typename * ?genericarg) + type2Unwrap <- '~' * S * >refName * ?genericarg do: + let named = namedType($1, move(userdata.wip.typeSlot.children)) + userdata.wip.typeSlot = CddlNode(kind: nkUnwrap, children: @[named]) + + # ('&' * S * '(' * S * group * S * ')') + type2GroupEnum <- type2GroupEnumPush * group * S * ')' do: + userdata.closeContainer(nkGroup) + let grp = move(userdata.wip.typeSlot) + userdata.wip.typeSlot = CddlNode(kind: nkEnum, children: @[grp]) + + type2GroupEnumPush <- '&' * S * '(' * S do: + userdata.openContainer() + + # ('&' * S * groupname * ?genericarg) + type2GroupName <- '&' * S * >refName * ?genericarg do: + let named = namedType($1, move(userdata.wip.typeSlot.children)) + userdata.wip.typeSlot = CddlNode(kind: nkEnum, children: @[named]) + + # ('#' * '6' * ?('.' * uint) * '(' * S * typ * S * ')') + type2Tag <- '#' * >('6' * ?('.' * uint)) * '(' * S * typ * S * ')' do: + let inner = move(userdata.wip.typeSlot) + userdata.wip.typeSlot = + CddlNode(kind: nkTagged, val: majorVal($1, userdata.error), children: @[inner]) + + # ('#' * DIGIT * ?('.' * uintx)) + type2Major <- '#' * >(DIGIT * ?('.' * uint)) do: + userdata.wip.typeSlot = CddlNode(kind: nkMajor, val: majorVal($1, userdata.error)) + + # (#) + type2Any <- '#' do: + userdata.wip.typeSlot = CddlNode(kind: nkAny) + + # genericarg <- '<' * S * type1 * S * *(',' * S * type1 * S) * '>' + genericarg <- + genericargPush * genericargType1 * S * *(',' * S * genericargType1 * S) * '>' do: + # the arguments stay in the type slot, for the name in front to take + userdata.closeContainer(nkUnset) + + genericargPush <- '<' * S do: + userdata.openContainer() + + genericargType1 <- >type1 do: + userdata.members.add move(userdata.wip.typeSlot) + + # genericparm <- '<' * S * id * S * *(',' * S * id * S) * '>' + genericparm <- '<' * S * genericparmId * S * *(',' * S * genericparmId * S) * '>' + + genericparmId <- >id do: + userdata.ruleParams.add CddlNode(kind: nkParam, val: CddlNodeVal(text: $1)) + + # occur <- ?uint * '*' * ?uint | '+' | '?' + occur <- occurRange | occurOneOrMore | occurOptional + + occurRange <- >?uint * '*' * >?uint do: + userdata.wip.val.occur.kind = + if ($1).len + ($2).len > 0: ocRange else: ocZeroOrMore + if ($1).len > 0: + userdata.wip.val.occur.lo = parseNum($1, userdata.error, "occurrence bound") + if ($2).len > 0: + userdata.wip.val.occur.hi = parseNum($2, userdata.error, "occurrence bound") + + occurOneOrMore <- '+' do: + userdata.wip.val.occur.kind = ocOneOrMore + + occurOptional <- '?' do: + userdata.wip.val.occur.kind = ocOptional + + # groupname or typename + refName <- id do: + reset(userdata.wip.typeSlot) + + bareword <- id + typename <- id + groupname <- id + + value <- number | text | bytes + text <- '"' * *SCHAR * '"' + bytes <- ?bsqual * '\'' * *BCHAR * '\'' + bsqual <- 'h' | "b64" + rangeop <- "..." | ".." + ctlop <- '.' * id + id <- EALPHA * *(*('-' | '.') * (EALPHA | DIGIT)) + number <- hexfloat | (int * ?('.' * fraction) * ?('e' * exponent)) + hexfloat <- ?'-' * "0x" * +HEXDIG * ?('.' * +HEXDIG) * 'p' * exponent + int <- ?'-' * uint + uint <- (DIGIT1 * *DIGIT) | ("0x" * +HEXDIG) | ("0b" * +BINDIG) | "0" + fraction <- +DIGIT + exponent <- ?('+' | '-') * +DIGIT + SCHAR <- + {'\x20' .. '\x21', '\x23' .. '\x5B', '\x5D' .. '\x7E', '\x80' .. '\xFF'} | SESC + BCHAR <- + {'\x20' .. '\x26', '\x28' .. '\x5B', '\x5D' .. '\x7E', '\x80' .. '\xFF'} | SESC | + CRLF + SESC <- '\\' * {'\x20' .. '\x7E', '\x80' .. '\xFF'} + + S <- *WS + WS <- SP | NL + SP <- ' ' + NL <- COMMENT | NEWLINE + COMMENT <- >(';' * *PCHAR) * CRLF do: + userdata.addComment($1, @1) + NEWLINE <- >CRLF do: + userdata.addBlank(@1) + PCHAR <- {'\x20' .. '\x7E', '\x80' .. '\xFF'} + CRLF <- ('\x0D' * '\x0A') | '\x0A' + + BINDIG <- '0' | '1' + HEXDIG <- DIGIT | {'A' .. 'F'} | {'a' .. 'f'} + DIGIT1 <- {'1' .. '9'} + DIGIT <- {'0' .. '9'} + EALPHA <- ALPHA | '@' | '_' | '$' + ALPHA <- {'A' .. 'Z'} | {'a' .. 'z'} + + var state = + ParseState(wip: initEntry(), source: source, ignoreComments: ignoreComments) + + let r = + try: + # XXX not unsafe, see https://github.com/zevv/npeg/issues/28 + {.cast(gcsafe).}: + parser.match(source, state) + except NPegException as exc: + raise newCddlError(source, exc.matchLen, exc.matchMax) + # match throws Exception error... + except CatchableError as exc: + raise (ref CborCddlError)(msg: "CBOR CDDL parser error: " & exc.msg, parent: exc) + except Defect as exc: + raise exc + except Exception: + raiseAssert "Unexpected Exception" + if state.error.len > 0: + raise (ref CborCddlError)(msg: "CBOR CDDL parser error: " & state.error) + if r.ok: + doAssert r.matchLen == source.len + state.flushNl(state.schema, blanks = true) + CddlNode(kind: nkSchema, children: move(state.schema)) + else: + raise newCddlError(source, r.matchLen, r.matchMax) + +{.push raises: [], gcsafe.} + +type WidthSink = object + ## Counts what would be written to it rather than writing it, so that a + ## choice can be measured without a rendering being built. The emitters that + ## take one are generic over their sink, so what is counted here and what is + ## written out can never come from two different pieces of code. + width: int + +proc write( + w: var WidthSink, s: string +) {.raises: [IOError], hint[XCannotRaiseY]: off.} = + w.width += s.len + +proc write(w: var WidthSink, c: char) {.raises: [IOError], hint[XCannotRaiseY]: off.} = + w.width += 1 + +func writeText( + w: var WidthSink, x: SomeUnsignedInt +) {.raises: [IOError], hint[XCannotRaiseY]: off.} = + var v = x + inc w.width + while v >= 10: + v = v div 10 + inc w.width + +proc emitInline[S](dst: var S, n: CddlNode) {.raises: [IOError].} + +proc inlineWidth(n: CddlNode): int {.raises: [IOError].} = + ## How wide emitInline would write this node, without writing it. + var w = WidthSink() + emitInline(w, n) + w.width + +proc emitParams[S](dst: var S, n: CddlNode, sep: string) {.raises: [IOError].} = + ## The "" a generic rule declares; nothing when it declares none. + if n.genericParams.len > 0: + dst.write '<' + for i in 0 ..< n.genericParams.len: + if i > 0: + dst.write sep + dst.write n.genericParams[i].val.text + dst.write '>' + +proc emitIn[S](dst: var S, n: CddlNode, needsParens = false) {.raises: [IOError].} = + ## Write a node in the parens the position it is used in calls for; the ones + ## written in the source are nkParen nodes and write themselves. + if needsParens: + dst.write '(' + emitInline(dst, n) + dst.write ')' + else: + emitInline(dst, n) + +proc emitList[S](dst: var S, nodes: seq[CddlNode], sep: string) {.raises: [IOError].} = + var first = true + for i in 0 ..< nodes.len: # by index: "for i, c in nodes" would copy each c + if nodes[i].kind notin nlKinds: # one would swallow what follows it + if not first: + dst.write sep + first = false + emitIn(dst, nodes[i]) + +proc emitOperand[S](dst: var S, n: CddlNode) {.raises: [IOError].} = + ## Operands of an operator are type2's, so anything composite needs parens + ## to read back the way it was written. + emitIn(dst, n, n.kind in {nkUnion, nkRange, nkControl}) + +proc emitOccur[S](dst: var S, o: CddlOccur) {.raises: [IOError].} = + case o.kind + of ocOne: + discard + of ocOptional: + dst.write '?' + of ocOneOrMore: + dst.write '+' + of ocZeroOrMore: + dst.write '*' + of ocRange: + if o.lo.isSome: + dst.writeText o.lo.get() + dst.write '*' + if o.hi.isSome: + dst.writeText o.hi.get() + +proc emitMajor[S](dst: var S, n: CddlNode) {.raises: [IOError].} = + ## The "6.32" or "3" an nkTagged or nkMajor was written as, without its '#'. + dst.writeText n.val.major + if n.val.minor.isSome: + dst.write '.' + dst.writeText n.val.minor.get() + +proc emitKeyPrefix[S](dst: var S, n: CddlNode) {.raises: [IOError].} = + case n.val.sepKind + of skNone: + discard + of skColon: + emitIn(dst, n.key) + dst.write ": " + of skArrow: + emitIn(dst, n.key) + dst.write " => " + of skArrowCut: + emitIn(dst, n.key) + dst.write " ^ => " + +proc emitInline[S](dst: var S, n: CddlNode) {.raises: [IOError].} = + case n.kind + of nkUnset: + dst.write "" + of nkSchema: + emitList(dst, n.children, " ") + of nkRule: + dst.write n.val.ruleText + emitParams(dst, n, ", ") + dst.write assignText(n.val.ruleKind) + emitIn(dst, n.body) + of nkEntry: + if n.val.occur.kind != ocOne: + emitOccur(dst, n.val.occur) + dst.write ' ' + emitKeyPrefix(dst, n) + emitIn(dst, n.body) + of nkParam, nkTypeRef, nkValue: + dst.write n.val.text + of nkMap: + dst.write '{' + emitList(dst, n.children, ", ") + dst.write '}' + of nkArray: + dst.write '[' + emitList(dst, n.children, ", ") + dst.write ']' + of nkGroup: + dst.write '(' + emitList(dst, n.children, ", ") + dst.write ')' + of nkGroupChoice: + # the alternatives are groups, but their entries are written out bare: + # the "( ... )" of the enclosing container is the one that delimits them + for i in 0 ..< n.children.len: + if i > 0: + dst.write " // " + if n.children[i].kind == nkGroup: + emitList(dst, n.children[i].children, ", ") + else: + emitIn(dst, n.children[i]) + of nkUnion: + for i in 0 ..< n.children.len: + if i > 0: + dst.write " / " + emitIn(dst, n.children[i], n.children[i].kind == nkUnion) + of nkParen: + dst.write '(' + emitInline(dst, n.target) + dst.write ')' + of nkTagged: + dst.write '#' + emitMajor(dst, n) + dst.write '(' + emitIn(dst, n.target) + dst.write ')' + of nkMajor: + dst.write '#' + emitMajor(dst, n) + of nkAny: + dst.write '#' + of nkGeneric: + dst.write n.val.text + dst.write '<' + emitList(dst, n.children, ", ") + dst.write '>' + of nkRange: + emitOperand(dst, n.lhs) + dst.write rangeOpText(n.val.rangeKind) + emitOperand(dst, n.rhs) + of nkControl: + emitOperand(dst, n.lhs) + dst.write ' ' + dst.write n.val.text + dst.write ' ' + emitOperand(dst, n.rhs) + of nkUnwrap: + dst.write '~' + emitIn(dst, n.target) + of nkEnum: + dst.write '&' + emitIn(dst, n.target) + of nkComment, nkCommentInline: + dst.write n.val.text # on its own; among other nodes emitList leaves it out + of nkEmpty: + discard # a line with nothing on it has nothing to write on one line + +template intoString(body: untyped): string = + ## Run the emitters against a memory stream and hand back what they wrote, + ## or against a string when there is no stream to be had. + var stream = memoryOutput() + var dst {.inject.}: OutputStream = stream + try: + body + except IOError: + raiseAssert "memoryOutput is exception-free" + stream.getOutput(string) + +proc occurText*(o: CddlOccur): string = + ## The "?", "+", "*" or "n*m" an occurrence was written as; empty for ocOne. + intoString emitOccur(dst, o) + +proc majorText*(n: CddlNode): string = + ## The "6.32" or "3" an nkTagged or nkMajor was written as, without its '#'. + doAssert n.kind in {nkTagged, nkMajor} + intoString emitMajor(dst, n) + +const + prettyIndent = " " ## one level of indentation + prettyWrap* = 80 + ## the column a type choice is given before it is broken over lines of its + ## own. Maps, arrays and groups are structure and always break; a choice is + ## an expression, and one of two variants reads worse for being split up + +func prettyDelims(k: CddlNodeKind): (char, char) = + case k + of nkMap: + ('{', '}') + of nkArray: + ('[', ']') + else: + ('(', ')') + +proc emitIndent(dst: var OutputStream, indent: int) {.raises: [IOError].} = + for _ in 0 ..< indent: + dst.write prettyIndent + +proc emitPretty(dst: var OutputStream, n: CddlNode, indent: int) {.raises: [IOError].} + +proc emitPrettyIn( + dst: var OutputStream, n: CddlNode, indent: int, needsParens = false +) {.raises: [IOError].} = + ## As emitIn, for a node that may still hold a container to be broken open. + if needsParens: + dst.write '(' + emitPretty(dst, n, indent) + dst.write ')' + else: + emitPretty(dst, n, indent) + +proc emitPrettyOperand( + dst: var OutputStream, n: CddlNode, indent: int +) {.raises: [IOError].} = + emitPrettyIn(dst, n, indent, n.kind in {nkUnion, nkRange, nkControl}) + +proc emitLines( + dst: var OutputStream, nodes: seq[CddlNode], indent: int, sep: string +) {.raises: [IOError].} = + var onLine = false + for i in 0 ..< nodes.len: + if nodes[i].kind == nkCommentInline and onLine: + dst.write ' ' + dst.write nodes[i].val.text + continue + if onLine: + dst.write '\n' + onLine = true + if nodes[i].kind == nkEmpty: + continue # indenting a line with nothing on it would only trail spaces + emitIndent(dst, indent) + if nodes[i].kind in commentKinds: + dst.write nodes[i].val.text + else: + emitPretty(dst, nodes[i], indent) + dst.write sep + if onLine: + dst.write '\n' + +proc emitPretty(dst: var OutputStream, n: CddlNode, indent: int) {.raises: [IOError].} = + ## Write a node with every container it holds broken open, one member to a + ## line, indented a level further in for each container it sits inside. + ## Anything that is not a container is written the way emitInline writes it. + case n.kind + of nkSchema: + emitLines(dst, n.children, indent, "") + of nkRule: + dst.write n.val.ruleText + emitParams(dst, n, ", ") + dst.write assignText(n.val.ruleKind) + emitPretty(dst, n.body, indent) + of nkEntry: + if n.val.occur.kind != ocOne: + emitOccur(dst, n.val.occur) + dst.write ' ' + emitKeyPrefix(dst, n) + emitPretty(dst, n.body, indent) + of nkMap, nkArray, nkGroup: + let (open, close) = prettyDelims(n.kind) + dst.write open + if n.children.len == 1 and n.children[0].kind notin nlKinds and + n.children[0].kind != nkGroupChoice and n.children[0].val.sepKind == skNone: + emitPretty(dst, n.children[0], indent) + elif n.children.len > 0: + # a choice writes out its own alternatives, and a comma after the last + # of them would read as one more entry of that alternative + var isChoice = false + for i in 0 ..< n.children.len: + if n.children[i].kind == nkGroupChoice: + isChoice = true + dst.write '\n' + emitLines(dst, n.children, indent + 1, if isChoice: "" else: ",") + emitIndent(dst, indent) + dst.write close + of nkGroupChoice: + # each alternative stays on a line of its own: breaking their entries out + # too would leave the "//" between them looking like it separates entries + for i in 0 ..< n.children.len: + if i > 0: + dst.write " //\n" + emitIndent(dst, indent) + if n.children[i].kind == nkGroup: + emitList(dst, n.children[i].children, ", ") + else: + emitIn(dst, n.children[i]) + of nkParen: + dst.write '(' + emitPretty(dst, n.target, indent) + dst.write ')' + of nkTagged: + dst.write '#' + emitMajor(dst, n) + dst.write '(' + emitPretty(dst, n.target, indent) + dst.write ')' + of nkUnwrap: + dst.write '~' + emitPretty(dst, n.target, indent) + of nkEnum: + dst.write '&' + emitPretty(dst, n.target, indent) + of nkUnion: + # a short choice stays on its line; a long one gets a variant per line, the + # first carrying on from whatever the choice was written after. What the + # head of that line already took is not counted, so the wrap is a little + # later than it looks - close enough for something with no line of its own + if indent * prettyIndent.len + inlineWidth(n) <= prettyWrap: + emitInline(dst, n) + else: + for i in 0 ..< n.children.len: + if i > 0: + dst.write " /\n" + emitIndent(dst, indent + 1) + emitPrettyIn(dst, n.children[i], indent + 1, n.children[i].kind == nkUnion) + of nkRange: + emitPrettyOperand(dst, n.lhs, indent) + dst.write rangeOpText(n.val.rangeKind) + emitPrettyOperand(dst, n.rhs, indent) + of nkControl: + emitPrettyOperand(dst, n.lhs, indent) + dst.write ' ' + dst.write n.val.text + dst.write ' ' + emitPrettyOperand(dst, n.rhs, indent) + of nkGeneric: + dst.write n.val.text + dst.write '<' + var first = true + for i in 0 ..< n.children.len: + if n.children[i].kind notin nlKinds: + if not first: + dst.write ", " + first = false + emitPrettyIn(dst, n.children[i], indent) + dst.write '>' + else: + # a value, a reference, a major type or a comment: nothing below to open + emitInline(dst, n) + +func sameTree(a, b: CddlNode, withNl: bool): bool = + ## Whether the two are the same schema. Without withNl the comments and + ## blank lines on either side are passed over, which is what a rendering that + ## leaves them out has to be held to. + if a.kind != b.kind or a.val != b.val: + return false + var ai, bi = 0 + while true: + if not withNl: + while ai < a.children.len and a.children[ai].kind in nlKinds: + inc ai + while bi < b.children.len and b.children[bi].kind in nlKinds: + inc bi + if ai >= a.children.len or bi >= b.children.len: + return ai >= a.children.len and bi >= b.children.len + if not sameTree(a.children[ai], b.children[bi], withNl): + return false + inc ai + inc bi + +proc verify(n: CddlNode, cddl: string, pretty: bool) {.raises: [CborCddlError].} = + ## Parse the cddl and check the result is equal to `n`. + if n.kind notin {nkSchema, nkRule}: + raise (ref CborCddlError)(msg: "The CDDL must be a schema or a rule") + let back = parseCddl(cddl) + let same = + if n.kind == nkSchema: + sameTree(n, back, pretty) + else: + # a rule is written as the one rule of a schema, so unwrap it to compare + back.entryCount == 1 and sameTree(n, back.firstEntry, pretty) + if not same: + raise (ref CborCddlError)(msg: "Invalid CDDL result; it does not match the input") + +proc toCddl*( + n: CddlNode, pretty = false, verify = true +): string {.raises: [CborCddlError].} = + ## Render a node as the CDDL it was parsed from. If `verify` + ## is true, the `n` will be checked to match the resulting cddl. + ## The verification is expensive, so if this is called at runtime + ## it's good to enabled it only in debug mode. + let ret = + if pretty: + intoString emitPretty(dst, n, 0) + else: + intoString emitInline(dst, n) + if verify: + verify(n, ret, pretty) + ret + +proc emitLabel(dst: var OutputStream, n: CddlNode) {.raises: [IOError].} = + case n.kind + of nkUnset: + dst.write "" + of nkSchema: + dst.write "Schema" + of nkRule: + # the parameters are shown here rather than as children of their own + dst.write "Rule " + dst.write n.val.ruleText + emitParams(dst, n, ",") + dst.write " [" + dst.write $n.val.ruleKind + dst.write ']' + of nkEntry: + dst.write "Entry [" + dst.write $n.val.occur.kind + dst.write ']' + if n.val.occur.kind == ocRange: + dst.write ' ' + emitOccur(dst, n.val.occur) + if n.val.sepKind != skNone: + dst.write " key(" # the key is the first child + dst.write $n.val.sepKind + dst.write ')' + of nkParam: + dst.write "Param(" + dst.write n.val.text + dst.write ')' + of nkTypeRef: + dst.write "TypeRef(" + dst.write n.val.text + dst.write ')' + of nkValue: + dst.write "Value(" + dst.write n.val.text + dst.write ')' + of nkMap: + dst.write "Map" + of nkArray: + dst.write "Array" + of nkGroup: + dst.write "Group" + of nkGroupChoice: + dst.write "GroupChoice" + of nkUnion: + dst.write "Union" + of nkTagged: + dst.write "Tagged(#" + emitMajor(dst, n) + dst.write ')' + of nkMajor: + dst.write "Major(#" + emitMajor(dst, n) + dst.write ')' + of nkAny: + dst.write "Any" + of nkGeneric: + dst.write "Generic(" + dst.write n.val.text + dst.write ')' + of nkRange: + dst.write "Range(" + dst.write rangeOpText(n.val.rangeKind) + dst.write ')' + of nkControl: + dst.write "Control(" + dst.write n.val.text + dst.write ')' + of nkParen: + dst.write "Paren" + of nkUnwrap: + dst.write "Unwrap" + of nkEnum: + dst.write "Enum" + of nkComment: + dst.write "Comment(" + dst.write n.val.text + dst.write ')' + of nkCommentInline: + dst.write "CommentInline(" + dst.write n.val.text + dst.write ')' + of nkEmpty: + dst.write "Empty" + +proc emitTree(dst: var OutputStream, n: CddlNode, indent: int) {.raises: [IOError].} = + emitIndent(dst, indent) + emitLabel(dst, n) + for i in 0 ..< n.children.len: + if n.children[i].kind != nkParam: # a rule shows those in its own label + dst.write '\n' + emitTree(dst, n.children[i], indent + 1) + +proc dumpTree*(n: CddlNode): string = + ## Indented tree rendering of a node and all of its children. + intoString emitTree(dst, n, 0) + +func allIn( + nodes: seq[CddlNode], kinds: set[CddlNodeKind], first = 0, last = int.high +): bool = + ## Whether every node in first .. last is of one of these kinds. Taken by + ## index rather than as a slice, which would copy what it was handed. + for i in first .. min(last, nodes.high): + if nodes[i].kind notin kinds: + return false + true + +func isValid*(n: CddlNode): bool = + ## Valid a CddlNode tree is constructed correctly + # - nkUnset: never valid; it's an unfilled slot, no part of a finished tree + # - nkSchema: only rules, comments and blank lines, no two blanks in a row + # - nkRule: zero or more nkParam, then exactly one body; a group rule binds + # an nkEntry, a type rule binds a type + # - nkEntry: 1 child under skNone, 2 otherwise, body a type; under skColon + # the key must be nkTypeRef or nkValue, under the arrows any type + # - nkMap/nkArray/nkGroup: entries and comments, or exactly one nkGroupChoice and no entries + # - nkGroupChoice: >=2 alternatives, all nkGroup (one would have been unwrapped) + # - nkUnion: >=2 variants, all types + # - nkTagged: one type, and major == 6, since the grammar admits no other tag + # - nkRange/nkControl: exactly two type operands + # - nkParen/nkUnwrap/nkEnum: exactly one type + # - leaves: no children + for i in 0 ..< n.children.len: + if not isValid(n.children[i]): + return false + let last = n.children.high + case n.kind + of nkUnset: + false + of nkSchema: + var ok = allIn(n.children, {nkRule} + nlKinds) + for i in 1 ..< n.children.len: + # a run of blank lines is the one break, so two in a row never stand + if n.children[i].kind == nkEmpty and n.children[i - 1].kind == nkEmpty: + ok = false + ok + of nkRule: + if n.children.len == 0 or not allIn(n.children, {nkParam}, last = last - 1): + false + elif n.val.ruleKind in groupRules: + n.children[last].kind == nkEntry + else: + n.children[last].kind in typeKinds + of nkEntry: + if n.val.sepKind == skNone: + n.children.len == 1 and n.children[0].kind in typeKinds + elif n.children.len != 2 or n.children[1].kind notin typeKinds: + false + elif n.val.sepKind == skColon: + n.children[0].kind in {nkTypeRef, nkValue} + else: + n.children[0].kind in typeKinds + of nkMap, nkArray, nkGroup: + var choices = 0 + var members = 0 + for i in 0 ..< n.children.len: + case n.children[i].kind + of nkGroupChoice: + inc choices + of nkEntry: + inc members + of nkComment, nkCommentInline: + discard + else: + return false + choices == 0 or (choices == 1 and members == 0) + of nkGroupChoice: + n.children.len >= 2 and allIn(n.children, {nkGroup}) + of nkUnion: + n.children.len >= 2 and allIn(n.children, typeKinds) + of nkGeneric: + n.children.len >= 1 and allIn(n.children, typeKinds + commentKinds) and + n.entryCount >= 1 + of nkTagged: + n.children.len == 1 and n.children[0].kind in typeKinds and n.val.major == 6 + of nkRange, nkControl: + n.children.len == 2 and allIn(n.children, typeKinds) + of nkParen, nkUnwrap, nkEnum: + n.children.len == 1 and n.children[0].kind in typeKinds + of nkParam, nkTypeRef, nkValue, nkMajor, nkAny, nkComment, nkCommentInline, nkEmpty: + n.children.len == 0 diff --git a/cbor_serialization/cddl/parser2_schema.md b/cbor_serialization/cddl/parser2_schema.md new file mode 100644 index 0000000..d201d90 --- /dev/null +++ b/cbor_serialization/cddl/parser2_schema.md @@ -0,0 +1,121 @@ +# Parser2 schema + +Ideally this would live in a test, `type_generator` would parse it and test it VS the +`CddlNode` in `./parser2.nim`. Because it uses a union, it's not possible. +`type_generator` does not have support for union -> Nim case object, yet. + +```text +; CDDL describing the CDDL parser's own AST, ie the CddlNode of +; cbor_serialization/cddl/parser2.nim, as it would be serialized to CBOR. +; +; The generator turns each rule below into the Nim type of the same name, so +; the names here are the ones parser2 declares. An enum is written as one rule +; per value, bound to the ordinal, plus a rule choosing between them. + +; ---------------------------------------------------------------- occurrence + +ocOne = 0 ; no occurrence marker -> exactly 1 +ocOptional = 1 ; "?" +ocOneOrMore = 2 ; "+" +ocZeroOrMore = 3 ; "*" +ocRange = 4 ; n*m (lo / hi); hi may be absent + +CddlOccurKind = ocOne / ocOptional / ocOneOrMore / ocZeroOrMore / ocRange + +; both bounds are only set for ocRange, and each only when it was written: +; "0*3" and "*3" mean the same, and are not the same +CddlOccur = { + kind: CddlOccurKind, + ? lo: uint, + ? hi: uint, +} + +; ---------------------------------------------------------------- member key + +skNone = 0 ; positional (no memberkey) +skColon = 1 ; value key: name: type "k": type 1: type +skArrow = 2 ; type1 key: type1 S "=>" type +skArrowCut = 3 ; type1 key: type1 S "^" S "=>" type + +CddlSepKind = skNone / skColon / skArrow / skArrowCut + +; ---------------------------------------------------------------------- rule + +rkType = 0 ; typename = type +rkTypeExt = 1 ; typename /= type (extends a type choice) +rkGroup = 2 ; groupname = grpent +rkGroupExt = 3 ; groupname //= grpent (extends a group choice) + +CddlRuleKind = rkType / rkTypeExt / rkGroup / rkGroupExt + +; --------------------------------------------------------------------- range + +rngInclusive = 0 ; ".." upper bound is part of the range +rngExclusive = 1 ; "..." upper bound is excluded from the range + +CddlRangeKind = rngInclusive / rngExclusive + +; ---------------------------------------------------------------- node kinds + +; the children each kind carries are listed after the syntax it stands for +nkUnset = 0 ; nothing; children: - +nkSchema = 1 ; a whole schema; children: rules +nkRule = 2 ; name ? assign body; children: params, body +nkEntry = 3 ; ?occur ?memberkey type; children: ?key, type +nkParam = 4 ; the a and b a rule declares in "id = ..."; children: - +nkTypeRef = 5 ; tstr, uint, MyType; children: - +nkValue = 6 ; 0, "ok", h'0a'; children: - +nkMap = 7 ; { ... }; children: members +nkArray = 8 ; [ ... ]; children: members +nkGroup = 9 ; ( ... ); children: members +nkGroupChoice = 10 ; grpchoice // grpchoice; children: alternatives, nkGroup +nkUnion = 11 ; type1 / type2 / ...; children: variants +nkTagged = 12 ; #6.N( ... ); children: type +nkMajor = 13 ; #N #N.M; children: - +nkAny = 14 ; bare #; children: - +nkGeneric = 15 ; id<...>; children: arguments +nkRange = 16 ; 0..10 0...10; children: lhs, rhs +nkControl = 17 ; bstr .size 16; children: lhs, rhs +nkParen = 18 ; ( type ); children: type +nkUnwrap = 19 ; ~id ~id<...>; children: type +nkEnum = 20 ; &id &id<...> &( ... ); children: type +nkComment = 21 ; a comment, on a line of its own; children: - +nkCommentInline = 22 ; a comment, after what was written before it; children: - +nkEmpty = 23 ; the blank lines between two rules of a schema, however + ; many were written in a row; children: - + +CddlNodeKind = nkUnset / nkSchema / nkRule / nkEntry / nkParam / nkTypeRef / + nkValue / nkMap / nkArray / nkGroup / nkGroupChoice / nkUnion / + nkTagged / nkMajor / nkAny / nkGeneric / nkRange / nkControl / + nkParen / nkUnwrap / nkEnum / nkComment / nkCommentInline / + nkEmpty + +; ---------------------------------------------------------------------- node + +CddlRuleVal = {ruleText: tstr, ruleKind: CddlRuleKind} + +CddlEntryVal = { + occur: CddlOccur, + sepKind: CddlSepKind, +} + +CddlRangeVal = {rangeKind: CddlRangeKind} + +; the token the node was written as, for the kinds whose payload is only that: +; nkParam the parameter name, nkTypeRef the type or group name, nkGeneric the +; base name, nkControl the operator name with its '.', nkValue the literal as +; written +CddlTextVal = {text: tstr} + +CddlMajorVal = {major: 0..7, ? minor: uint} + +CddlNodeVal = + CddlRuleVal / CddlEntryVal / CddlRangeVal / CddlTextVal / CddlMajorVal + +; A node is its kind, its children, and the payload that kind carries +CddlNode = { + kind: CddlNodeKind, + children: [* CddlNode], + ? val: CddlNodeVal, +} +``` diff --git a/cbor_serialization/cddl/type_generator.nim b/cbor_serialization/cddl/type_generator.nim index 5587bec..553ffc4 100644 --- a/cbor_serialization/cddl/type_generator.nim +++ b/cbor_serialization/cddl/type_generator.nim @@ -7,17 +7,20 @@ # This file may not be copied, modified, or distributed except according to # those terms. -import std/[macros, tables, strutils], stew/shims/macros as stewmacros, ./parser +{.push raises: [], gcsafe.} + +import std/[macros, tables, strutils], stew/shims/macros as stewmacros, ./parser2 export CborCddlError -const repeatedMap = {OccurKind.ocOneOrMore, OccurKind.ocZeroOrMore, OccurKind.ocRange} +const repeatedMap = + {CddlOccurKind.ocOneOrMore, CddlOccurKind.ocZeroOrMore, CddlOccurKind.ocRange} proc newCborCddlError(msg: string): ref CborCddlError = (ref CborCddlError)(msg: msg) # https://datatracker.ietf.org/doc/html/rfc8610#appendix-D -proc toSimpleNimTyp(s: string): NimNode = +proc toSimpleNimTyp(s: string): NimNode {.raises: [CborCddlError].} = case s of "any": ident"CborBytes" @@ -43,36 +46,51 @@ proc toSimpleNimTyp(s: string): NimNode = else: ident(s) -proc toNimTyp(ft: FieldType, isOptional = false): NimNode = +proc entriesOf(n: CddlNode): lent seq[CddlNode] {.raises: [CborCddlError].} = + ## Members of a map, array or group; a group choice has no Nim equivalent. + for c in n.children: + if c.kind == nkGroupChoice: + raise newCborCddlError("unsupported group choice in " & $n.kind) + n.children + +proc toNimTyp(node: CddlNode, isOptional = false): NimNode {.raises: [CborCddlError].} = + template n(): untyped = + node.unparen + let typ = - case ft.kind - of fkSimpleType: - toSimpleNimTyp(ft.name) - of fkArray: - if ft.fields.len != 1: - raise newCborCddlError("unsupported array of len: " & $ft.fields.len) - let inner = toNimTyp(ft.fields[0].typ) + case n.kind + of nkTypeRef: + toSimpleNimTyp(n.val.text) + of nkArray: + let entries = entriesOf(n) + if entries.len != 1: + raise newCborCddlError("unsupported array of len: " & $entries.len) + let inner = toNimTyp(entries[0].body) newNimNode(nnkBracketExpr).add(ident("seq"), inner) - of fkMap: - if ft.fields.len != 1: - raise newCborCddlError("unsupported map of len: " & $ft.fields.len) - if ft.fields[0].occur.kind notin repeatedMap: + of nkMap: + let entries = entriesOf(n) + if entries.len != 1: + raise newCborCddlError("unsupported map of len: " & $entries.len) + if entries[0].val.occur.kind notin repeatedMap: raise newCborCddlError("unsupported single key map") - let key = toSimpleNimTyp(ft.fields[0].keyText) - let val = toNimTyp(ft.fields[0].typ) + let keyNode = entries[0].key.unparen + if keyNode.kind != nkTypeRef: + raise newCborCddlError("unsupported map key " & $keyNode.kind) + let key = toSimpleNimTyp(keyNode.val.text) + let val = toNimTyp(entries[0].body) newNimNode(nnkBracketExpr).add(ident("Table"), key, val) else: - raise newCborCddlError("unsupported type " & $ft.kind) + raise newCborCddlError("unsupported type " & $n.kind) if isOptional: newNimNode(nnkBracketExpr).add(ident("Opt"), typ) else: typ -proc toLitNode(ft: FieldType): NimNode = +proc toLitNode(n: CddlNode): NimNode {.raises: [CborCddlError].} = template s(): untyped = - ft.valueText + n.val.text - doAssert ft.kind == fkValue + doAssert n.kind == nkValue doAssert s.len > 0 if s[0] == '"': doAssert s.len >= 2 @@ -86,17 +104,23 @@ proc toLitNode(ft: FieldType): NimNode = raise newCborCddlError("unsupported value " & s) newLitFixed(val) -proc toKeyNode(ft: Field): NimNode = - template s(): untyped = - ft.keyText - - doAssert s.len > 0 - if s[0] == '"': - doAssert s.len >= 2 - doAssert s[^1] == '"' - ident(s[1 ..< s.high]) +proc toKeyNode(n: CddlNode): NimNode {.raises: [CborCddlError].} = + ## The Nim field name a member key stands for. + let k = n.key.unparen + case k.kind + of nkTypeRef: # a bareword, or a name used as a key + ident(k.val.text) + of nkValue: + let s = k.val.text + doAssert s.len > 0 + if s[0] == '"': + doAssert s.len >= 2 + doAssert s[^1] == '"' + ident(s[1 ..< s.high]) + else: + ident(s) else: - ident(s) + raise newCborCddlError("unsupported member key " & $k.kind) proc toEnumFieldName(s: string, i: int): NimNode = if s.len >= 2 and s[0].isAlphaAscii and s[1].isAlphaAscii: @@ -104,68 +128,73 @@ proc toEnumFieldName(s: string, i: int): NimNode = else: ident("e" & $i) -proc literalsMap(cddl: CddlSchema): TableRef[string, FieldType] = +proc literalsMap(cddl: CddlSchema): TableRef[string, CddlNode] = ## map of rule_name -> literal_val - result = newTable[string, FieldType]() - for rule in cddl: - if rule.kind == rkType and rule.typeExpr.kind == fkValue: - result[rule.name] = rule.typeExpr + result = newTable[string, CddlNode]() + for rule in cddl.children: + if rule.val.ruleKind in typeRules and rule.body.unparen.kind == nkValue: + result[rule.val.ruleText] = rule.body.unparen -proc isOptional(f: Field): bool = - f.occur.kind == OccurKind.ocOptional +proc isOptional(n: CddlNode): bool = + n.val.occur.kind == CddlOccurKind.ocOptional proc fromCddlImpl*(s: string): NimNode {.raises: [CborCddlError].} = + template body(): untyped = + rule.body.unparen + result = newNimNode(nnkTypeSection) - let cddl = parseCddl(s) + let cddl = parseCddl(s, ignoreComments = true) let lits = literalsMap(cddl) - for rule in cddl: - doAssert rule.kind == rkType + for rule in cddl.children: + doAssert rule.kind == nkRule + doAssert rule.val.ruleKind in typeRules let value = - case rule.typeExpr.kind - of fkMap: - if rule.typeExpr.fields.len == 1 and - rule.typeExpr.fields[0].occur.kind in repeatedMap: - toNimTyp(rule.typeExpr) + case body.kind + of nkMap: + let entries = entriesOf(body) + if entries.len == 1 and entries[0].val.occur.kind in repeatedMap: + toNimTyp(body) else: let fields = newNimNode(nnkRecList) - for f in rule.typeExpr.fields: + for f in entries: fields.add newNimNode(nnkIdentDefs).add( newNimNode(nnkPostfix).add(ident("*"), toKeyNode(f)), - toNimTyp(f.typ, f.isOptional), + toNimTyp(f.body, f.isOptional), newEmptyNode(), ) newNimNode(nnkObjectTy).add(newEmptyNode(), newEmptyNode(), fields) - of fkUnion: + of nkUnion: var fields = default(seq[NimNode]) - for i, variant in rule.typeExpr.variants.pairs(): + for i, parenthesized in body.children.pairs(): + let variant = parenthesized.unparen case variant.kind - of fkSimpleType: - let v = lits.getOrDefault(variant.name, default(FieldType)) - if v.kind == fkUnset: - raise newCborCddlError("union variant not found: " & $variant.name) + of nkTypeRef: + let v = lits.getOrDefault(variant.val.text, default(CddlNode)) + if v.kind == nkUnset: + raise newCborCddlError("union variant not found: " & $variant.val.text) fields.add newNimNode(nnkEnumFieldDef).add( - ident(variant.name), toLitNode(v) + ident(variant.val.text), toLitNode(v) ) - of fkValue: + of nkValue: fields.add newNimNode(nnkEnumFieldDef).add( - toEnumFieldName(rule.name, i), toLitNode(variant) + toEnumFieldName(rule.val.ruleText, i), toLitNode(variant) ) else: raise newCborCddlError("unsupported type " & $variant.kind) newNimNode(nnkEnumTy).add(newEmptyNode()).add(fields) - of fkSimpleType, fkArray: - toNimTyp(rule.typeExpr) - of fkValue: + of nkTypeRef, nkArray: + toNimTyp(body) + of nkValue: default(NimNode) # lits map contains this field else: - raise newCborCddlError("unsupported type " & $rule.typeExpr.kind) - case rule.typeExpr.kind - of fkValue: + raise newCborCddlError("unsupported type " & $body.kind) + case body.kind + of nkValue: discard - of fkUnion: + of nkUnion: result.add newNimNode(nnkTypeDef).add( newNimNode(nnkPragmaExpr).add( - newNimNode(nnkPostfix).add(ident("*"), ident(rule.name)), + newNimNode(nnkPostfix).add(ident("*"), ident(rule.val.ruleText)), newNimNode(nnkPragma).add(ident("pure")), ), newEmptyNode(), @@ -173,10 +202,14 @@ proc fromCddlImpl*(s: string): NimNode {.raises: [CborCddlError].} = ) else: result.add newNimNode(nnkTypeDef).add( - newNimNode(nnkPostfix).add(ident("*"), ident(rule.name)), newEmptyNode(), value + newNimNode(nnkPostfix).add(ident("*"), ident(rule.val.ruleText)), + newEmptyNode(), + value, ) when defined(CborLogGeneratedTypes): debugEcho repr(result) +{.pop.} + macro fromCddl*(s: static[string]): untyped = fromCddlImpl(s) diff --git a/tests/test_all.nim b/tests/test_all.nim index 6d98908..82dc88d 100644 --- a/tests/test_all.nim +++ b/tests/test_all.nim @@ -13,5 +13,9 @@ import ./[ test_spec, test_serialization, test_simple_value, test_cbor_flavor, test_parser, test_reader, test_writer, test_valueref, test_cbor_raw, test_malformed, test_std, - test_overloads, test_edn, test_cddl_parser, test_cddl_gen, + test_overloads, test_edn, test_cddl_gen, ] + +# XXX npeg crash in cpp https://github.com/zevv/npeg/issues/79 +when not defined(cpp): + import ./[test_cddl_parser_old, test_cddl_parser] diff --git a/tests/test_cddl_gen.nim b/tests/test_cddl_gen.nim index ee12ff8..4d295fe 100644 --- a/tests/test_cddl_gen.nim +++ b/tests/test_cddl_gen.nim @@ -7,6 +7,8 @@ # This file may not be copied, modified, or distributed except according to # those terms. +{.push raises: [], gcsafe.} + import std/[macros, strutils], unittest2, ../cbor_serialization/cddl/type_generator proc fixAst(ast: NimNode): NimNode = @@ -32,7 +34,7 @@ proc fixAst(ast: NimNode): NimNode = inspect(ast) -proc checkCddl(cddl: string, expected: NimNode) = +proc checkCddl(cddl: string, expected: NimNode) {.raises: [CborCddlError].} = let gened = fromCddlImpl(cddl.unindent) if gened != expected.fixAst: checkpoint("FAILED: Got: " & repr(gened) & "\nExpected: " & repr(expected.fixAst)) diff --git a/tests/test_cddl_parser.nim b/tests/test_cddl_parser.nim index e8092fa..3fe080f 100644 --- a/tests/test_cddl_parser.nim +++ b/tests/test_cddl_parser.nim @@ -7,7 +7,13 @@ # This file may not be copied, modified, or distributed except according to # those terms. -import std/[os, strutils], unittest2, ./utils, ../cbor_serialization/cddl/parser +{.push raises: [], gcsafe.} + +import + std/[os, strutils], + unittest2, + ../cbor_serialization, + ../cbor_serialization/cddl/parser2 const testSpecCases = [ """ @@ -459,6 +465,9 @@ const testCases = [ "response = { status: uint, payload: T }\npaged = { items: [* T], next-page: uint }", # bounded list generic "bounded-list = [1*100 T]", + # generic group rules: the type branch of the rule is tried and abandoned + "pair = (key: K, value: V)", + "$$ext //= (payload: T)", # result union generic "success = { ok: true, value: T }\nfailure = { ok: false, error: tstr }\nresult = success / failure", # .cbor byte string @@ -578,7 +587,7 @@ proc normalizeText(s: string): string = s.replace("\r\n", "\n").strip() suite "Test CDDL parser": - staticTest "parse valid test cases": + dualTest "parse valid test cases": for t in @testSpecCases & @testCases & @testBookCases: try: discard parseCddl(t) @@ -586,7 +595,7 @@ suite "Test CDDL parser": checkpoint("FAILED: " & t) fail() - staticTest "parse invalid test cases": + dualTest "parse invalid test cases": for t in invalidTestCases: try: discard parseCddl(t) @@ -595,35 +604,728 @@ suite "Test CDDL parser": except CborCddlError: discard - staticTest "schema dump": + dualTest "schema dump": var schemas = default(seq[CddlSchema]) - for t in testCases: + let allCases = @testSpecCases & @testCases & @testBookCases + for t in allCases: try: let schema = parseCddl(t) schemas.add schema except CborCddlError: checkpoint("FAILED (dump): " & t) fail() - if schemas.len == testCases.len: + if schemas.len == allCases.len: var dump = "" for schema in schemas: - for r in schema: - dump.add "Rule: " & r.name & " [" & $r.kind & "]\n" - if r.genericParams.len > 0: - dump.add " genericParams: " & $r.genericParams & "\n" - if r.typeExpr.kind != fkUnset: - dump.add showType(r.typeExpr, 1) & "\n" - elif r.groupEntries.len > 0: - dump.add " groupEntries: " & $r.groupEntries.len & " field(s)\n" + for r in schema.children: + dump.add dumpTree(r) & "\n" + if r.kind == nkRule: + dump.add " ; " & toCddl(r) & "\n" const dumpFile = currentSourcePath.parentDir() / "test_cddl_parser_dump.txt" const dumpContent = staticRead(dumpFile) if dump.normalizeText() != dumpContent.normalizeText(): checkpoint(dump) fail() - staticTest "parse result can be stored in a const": + dualTest "roundtrip": + var schemas = default(seq[CddlSchema]) + let allCases = @testSpecCases & @testCases & @testBookCases + for t in allCases: + try: + let schema = parseCddl(t) + schemas.add schema + except CborCddlError: + checkpoint("FAILED (dump): " & t) + fail() + if schemas.len == allCases.len: + var dump = "" + for schema in schemas: + dump.add toCddl(schema, pretty = true) + const dumpFile = + currentSourcePath.parentDir() / "test_cddl_parser_dump_pretty.txt" + const dumpContent = staticRead(dumpFile) + if dump.normalizeText() != dumpContent.normalizeText(): + checkpoint(dump) + fail() + + dualTest "roundtrip-ish": + # Sanity check without any commas, spaces and new-lines + let rep = [("\n", ""), (" ", ""), (",", "")] + for t in @testSpecCases & @testCases & @testBookCases: + let got = parseCddl(t).toCddl(pretty = true) & "\n" + if multiReplace(got, rep) != multiReplace(t, rep): + checkpoint("got: " & got & "\n\nexpected: " & t) + fail() + + dualTest "parse result can be stored in a const": const r = parseCddl("my-first-rule = int") - check r[0].name == "my-first-rule" + check r.children[0].val.ruleText == "my-first-rule" + +suite "Test CDDL Node": + dualTest "rangeop": + block: + let schema = parseCddl("small = 0..100") + template n(): untyped = + schema.children[0].body + + check n.kind == nkRange + check n.val.rangeKind == rngInclusive + check n.lhs.kind == nkValue + check n.lhs.val.text == "0" + check n.rhs.kind == nkValue + check n.rhs.val.text == "100" + block: + let schema = parseCddl("byte1 = 0...256") + template n(): untyped = + schema.children[0].body + + check n.kind == nkRange + check n.val.rangeKind == rngExclusive + check n.lhs.val.text == "0" + check n.rhs.val.text == "256" + block: + let schema = parseCddl("max-byte = 255\nbyte = 0..max-byte") + template n(): untyped = + schema.children[1].body + + check n.kind == nkRange + check n.rhs.kind == nkTypeRef + check n.rhs.val.text == "max-byte" + + dualTest "ctlop": + block: + let schema = parseCddl("ip4 = bstr .size 4") + template n(): untyped = + schema.children[0].body + + check n.kind == nkControl + check n.val.text == ".size" + check n.lhs.kind == nkTypeRef + check n.lhs.val.text == "bstr" + check n.rhs.kind == nkValue + check n.rhs.val.text == "4" + block: + let schema = parseCddl("user = uint\nencoded = bstr .cbor user") + template n(): untyped = + schema.children[1].body + + check n.kind == nkControl + check n.val.text == ".cbor" + check n.rhs.kind == nkTypeRef + check n.rhs.val.text == "user" + block: + # the nested range must not clobber the lhs of the enclosing ctlop + let schema = parseCddl("label = tstr .size (1..63)") + template n(): untyped = + schema.children[0].body + + check n.kind == nkControl + check n.val.text == ".size" + check n.lhs.val.text == "tstr" + check n.rhs.kind == nkParen # as written + template arg(): untyped = + n.rhs.unparen + + check arg.kind == nkRange + check arg.lhs.val.text == "1" + check arg.rhs.val.text == "63" + block: + # ... nor of an enclosing ctlop + let schema = parseCddl("nested = tstr .size (uint .lt 8)") + template n(): untyped = + schema.children[0].body + + check n.kind == nkControl + check n.val.text == ".size" + check n.lhs.val.text == "tstr" + template arg(): untyped = + n.rhs.unparen + + check arg.kind == nkControl + check arg.val.text == ".lt" + check arg.lhs.val.text == "uint" + check arg.rhs.val.text == "8" + + dualTest "tags and major types": + block: + let schema = parseCddl("a = #6.32(tstr)\nb = #6(tstr)") + check schema.children[0].body.kind == nkTagged + check schema.children[0].body.val.major == 6 + check schema.children[0].body.val.minor == Opt.some(32'u64) + check schema.children[0].body.target.val.text == "tstr" + # "#6" carries no tag number, which is not the same as carrying zero + check schema.children[1].body.val.major == 6 + check schema.children[1].body.val.minor.isNone + block: + let schema = parseCddl("a = #3.5\nb = #3\nc = #") + check schema.children[0].body.kind == nkMajor + check schema.children[0].body.val.major == 3 + check schema.children[0].body.val.minor == Opt.some(5'u64) + check schema.children[1].body.val.major == 3 + check schema.children[1].body.val.minor.isNone + check schema.children[2].body.kind == nkAny + # each is dumped as it was written + check toCddl(schema.children[0]) == "a = #3.5" + check toCddl(schema.children[1]) == "b = #3" + block: + # the number is 64 bit, so a wider one is reported rather than truncated + check parseCddl("a = #6.18446744073709551615(tstr)").children[0].body.val.minor == + Opt.some(18446744073709551615'u64) + expect CborCddlError: + discard parseCddl("a = #6.18446744073709551616(tstr)") + + dualTest "unwrap": + block: + let schema = parseCddl("basic = [f1: int]\nadvanced = [~basic]") + template n(): untyped = + schema.children[1].body + + check n.kind == nkArray + check n.children[0].body.kind == nkUnwrap + check n.children[0].body.target.kind == nkTypeRef + check n.children[0].body.target.val.text == "basic" + block: + let schema = parseCddl("b = [t]\na = ~b") + template n(): untyped = + schema.children[1].body + + check n.kind == nkUnwrap + check n.target.kind == nkGeneric + check n.target.val.text == "b" + check n.target.children.len == 1 + check n.target.children[0].val.text == "uint" + + dualTest "group enumeration": + block: + let schema = parseCddl("basecolors = (black: 0)\ncolor = &basecolors") + template n(): untyped = + schema.children[1].body + + check n.kind == nkEnum + check n.target.kind == nkTypeRef + check n.target.val.text == "basecolors" + block: + let schema = parseCddl("extended = &(orange: 8, pink: 9)") + template n(): untyped = + schema.children[0].body + + check n.kind == nkEnum + check n.target.kind == nkGroup + check n.target.children.len == 2 + check n.target.children[0].key.val.text == "orange" + block: + let schema = parseCddl("b = (x: t)\na = &b") + template n(): untyped = + schema.children[1].body + + check n.kind == nkEnum + check n.target.kind == nkGeneric + check n.target.val.text == "b" + + dualTest "operators are not generics": + const expected = { + "small = 0..100": nkRange, + "ip4 = bstr .size 4": nkControl, + "advanced = ~basic": nkUnwrap, + "color = &basecolors": nkEnum, + "extended = &(orange: 8)": nkEnum, + "inst = message": nkGeneric, + } + for (t, kind) in expected: + check parseCddl(t).children[0].body.kind == kind + + dualTest "type choices nest": + block: + let schema = parseCddl("a = 1 / (2 / 3) / 4") + template n(): untyped = + schema.children[0].body + + check n.kind == nkUnion + check n.children.len == 3 + check n.children[0].val.text == "1" + check n.children[1].kind == nkParen # as written + check n.children[1].unparen.kind == nkUnion + check n.children[1].unparen.children.len == 2 + check n.children[2].val.text == "4" + block: + let schema = parseCddl("a = 1 / #6.32(2 / 3)") + template n(): untyped = + schema.children[0].body + + check n.kind == nkUnion + check n.children.len == 2 + check n.children[1].kind == nkTagged + check n.children[1].target.kind == nkUnion + + dualTest "rule kinds": + let schema = + parseCddl("attire = \"a\"\nattire /= \"b\"\nfoo = (x: 1)\nfoo //= (y: 2)") + check schema.children[0].val.ruleKind == rkType + check schema.children[1].val.ruleKind == rkTypeExt + check schema.children[2].val.ruleKind == rkGroup + check schema.children[3].val.ruleKind == rkGroupExt + # a group rule binds an entry, a type rule binds a type + check schema.children[0].body.kind == nkValue + check schema.children[2].body.kind == nkEntry + check schema.children[2].body.body.kind == nkGroup + + dualTest "occurrence bounds": + block: + let schema = parseCddl("a = [2*3 int]\nb = [*3 int]\nc = [2* int]\nd = [* int]") + template occ(i: int): untyped = + schema.children[i].body.children[0].val.occur + + # a bound is only set when it was written: "0*3" and "*3" differ + check occ(0).kind == ocRange + check occ(0).lo == Opt.some(2'u64) + check occ(0).hi == Opt.some(3'u64) + check occ(1).lo.isNone and occ(1).hi == Opt.some(3'u64) + check occ(2).lo == Opt.some(2'u64) and occ(2).hi.isNone + check occ(3).kind == ocZeroOrMore + block: + # a bound too large to hold is reported, not truncated or wrapped + check parseCddl("a = [18446744073709551615*2 int]").children[0].body.children[0].val.occur.lo == + Opt.some(18446744073709551615'u64) + for src in [ + "a = [18446744073709551616*2 int]", "a = [2*99999999999999999999 int]" + ]: + expect CborCddlError: + discard parseCddl(src) + + dualTest "generic rule parameters": + let schema = parseCddl("dict = {* k => v}\nplain = int") + template rule(): untyped = + schema.children[0] + + # the parameters are children of the rule, the body is the last one + check rule.genericParams.len == 2 + check rule.genericParams[0].kind == nkParam + check rule.genericParams[0].val.text == "k" + check rule.genericParams[1].val.text == "v" + check rule.body.kind == nkMap + check toCddl(rule) == "dict = {* k => v}" + # a rule that declares none has the body as its only child + check schema.children[1].genericParams.len == 0 + check schema.children[1].children.len == 1 + + dualTest "a generic group rule reads its parameters once": + # the type branch of the rule is tried first and abandoned, which reads the + # parameters a second time unless the rule head drops what it read + const expected = {"pair = (x: k, y: v)": 2, "$$ext //= (payload: t)": 1} + for (src, params) in expected: + let schema = parseCddl(src) + check schema.children[0].val.ruleKind in groupRules + check schema.children[0].genericParams.len == params + check toCddl(schema.children[0]) == src + + dualTest "a failed type branch does not leak a container": + # "(" is tried as a parenthesized type first; when it turns out to start a + # group instead, the entries must still land in the group, and not in what + # the abandoned branch left behind + let schema = parseCddl("$$tcp-option //= ( sack: [+(left: uint, right: uint)] )") + template rule(): untyped = + schema.children[0] + + check rule.val.ruleKind == rkGroupExt + check rule.body.kind == nkEntry + check rule.body.val.occur.kind == ocOne + template grp(): untyped = + rule.body.body + + check grp.kind == nkGroup + check grp.children.len == 1 + check grp.children[0].key.val.text == "sack" + check grp.children[0].val.occur.kind == ocOne + template arr(): untyped = + grp.children[0].body + + check arr.kind == nkArray + check arr.children.len == 1 + check arr.children[0].val.occur.kind == ocOneOrMore + check arr.children[0].body.kind == nkGroup + check arr.children[0].body.children.len == 2 + + dualTest "cut member keys": + let schema = parseCddl("m = { \"k\" ^ => int, \"j\" => int, n: uint }") + template m(): untyped = + schema.children[0].body + + check m.kind == nkMap + check m.children[0].val.sepKind == skArrowCut + check m.children[1].val.sepKind == skArrow + check m.children[2].val.sepKind == skColon + check m.children[0].hasCut + check not m.children[1].hasCut + check m.children[2].hasCut # the colon form cuts too + check toCddl(schema.children[0]) == "m = {\"k\" ^ => int, \"j\" => int, n: uint}" + + dualTest "parens around a type": + block: + let schema = parseCddl("odd = (uint)") + template n(): untyped = + schema.children[0].body + + check n.kind == nkParen + check n.target.kind == nkTypeRef + check n.target.val.text == "uint" + check n.unparen.val.text == "uint" + block: + let schema = parseCddl("plain = uint") + template n(): untyped = + schema.children[0].body + + check n.kind == nkTypeRef + check n.unparen.kind == nkTypeRef + block: # they nest, rather than collapsing into a flag + let schema = parseCddl("a = 1\nd = ((a))") + template n(): untyped = + schema.children[1].body + + check n.kind == nkParen + check n.target.kind == nkParen + check n.target.target.kind == nkTypeRef + check toCddl(schema.children[1]) == "d = ((a))" + + dualTest "group choices": + block: + let schema = parseCddl("m = { a: 1 // b: 2 }") + template n(): untyped = + schema.children[0].body + + check n.kind == nkMap + check n.children.len == 1 + template choice(): untyped = + n.children[0] + + check choice.kind == nkGroupChoice + check choice.children.len == 2 + check choice.children[0].kind == nkGroup + check choice.children[0].children.len == 1 + check choice.children[0].children[0].key.val.text == "a" + check choice.children[1].children[0].key.val.text == "b" + block: + # each alternative keeps its own entries, occurrences and all + let schema = parseCddl( + "delivery = (street: tstr, ? number: uint // po-box: uint // per-pickup: true)" + ) + template n(): untyped = + schema.children[0].body.body + + check n.kind == nkGroup + check n.children.len == 1 + template choice(): untyped = + n.children[0] + + check choice.kind == nkGroupChoice + check choice.children.len == 3 + check choice.children[0].children.len == 2 + check choice.children[0].children[1].val.occur.kind == ocOptional + check choice.children[1].children.len == 1 + check choice.children[1].children[0].key.val.text == "po-box" + check choice.children[2].children.len == 1 + block: + # a single grpchoice stays a flat member list + let schema = parseCddl("g = (a: 1, b: 2)") + template n(): untyped = + schema.children[0].body.body + + check n.kind == nkGroup + check n.children.len == 2 + check n.children[0].kind == nkEntry + block: + # an empty alternative is an empty group + let schema = parseCddl("g = ( // a: 1)") + template n(): untyped = + schema.children[0].body.body + + check n.children[0].kind == nkGroupChoice + check n.children[0].children.len == 2 + check n.children[0].children[0].children.len == 0 + block: + # a nested choice does not swallow the entries of the enclosing one + let schema = parseCddl("outer = ( (a: 1 // b: 2) // c: 3 )") + template n(): untyped = + schema.children[0].body.body + + template choice(): untyped = + n.children[0] + + check choice.kind == nkGroupChoice + check choice.children.len == 2 + check choice.children[1].children[0].key.val.text == "c" + template inner(): untyped = + choice.children[0].children[0].body + + check inner.kind == nkGroup + check inner.children[0].kind == nkGroupChoice + check inner.children[0].children.len == 2 + block: + # arrays and "&( ... )" carry group choices too + let schema = parseCddl("t = [ a: 1 // b: 2 ]") + template arr(): untyped = + schema.children[0].body + + check arr.kind == nkArray + check arr.children[0].kind == nkGroupChoice + let enumSchema = parseCddl("e = &(a: 1 // b: 2)") + template enm(): untyped = + enumSchema.children[0].body + + check enm.kind == nkEnum + check enm.target.kind == nkGroup + check enm.target.children[0].kind == nkGroupChoice + + dualTest "comments are nodes kept where they were written": + let schema = parseCddl( + "; about person\n" & "; and more about it\n" & "person = {\n" & " ; about name\n" & + " name: tstr, ; ends the name line\n" & " age: uint,\n" & "} ; ends the rule\n" + ) + # the ones written before the rule sit among the rules, not inside one + check schema.children.len == 4 + check schema.children[0].kind == nkComment + check schema.children[0].val.text == "; about person" + check schema.children[1].val.text == "; and more about it" + check schema.children[2].kind == nkRule + # it ended the line the rule ended on, so it is the inline kind + check schema.children[3].kind == nkCommentInline + check schema.children[3].val.text == "; ends the rule" + + template m(): untyped = + schema.children[2].body + + # and the ones inside the map sit among its members, in source order + check m.children.len == 4 + check m.entryCount == 2 # a comment is not a member + check m.children[0].kind == nkComment + check m.children[0].val.text == "; about name" + check m.children[1].kind == nkEntry + check m.children[2].kind == nkCommentInline + check m.children[2].val.text == "; ends the name line" + check m.children[3].kind == nkEntry + + dualTest "a comment after the last member stays inside its container": + ## The container is closed before the entry that owns it is finished, so a + ## comment read just before the "}" has to be written into the members + ## while they are still reachable. + let schema = parseCddl("outer = {inner: {y: int ; here\n}}\n") + + template inner(): untyped = + schema.children[0].body.firstEntry.body + + check inner.children.len == 2 + check inner.children[1].kind == nkCommentInline + check inner.children[1].val.text == "; here" + + dualTest "comments survive a branch the parser abandons": + ## The head of a rule is read again when its type branch fails, and a + ## comment the abandoned branch read is read again with it; neither may + ## leave a second copy behind, nor land in front of the rule. + let schema = parseCddl("; about g\ng = (a: int) ; trailing\n") + check schema.children.len == 3 + check schema.children[1].val.ruleKind == rkGroup # ie the type branch was abandoned + check schema.children[0].val.text == "; about g" + check schema.children[2].val.text == "; trailing" + + dualTest "the blank lines a schema is laid out with are kept": + ## What a schema puts between its rules - a heading, a blank line, then the + ## comment on the rule that follows - is what the nkEmpty nodes are for. + let source = + "; a schema\n\n; about a\na = int\nb = int\n\n; about c\nc = int\n\nd = int\n" + let schema = parseCddl(source) + check schema.children.len == 10 + let kinds = [ + nkComment, nkEmpty, nkComment, nkRule, nkRule, nkEmpty, nkComment, nkRule, + nkEmpty, nkRule, + ] + for i in 0 ..< kinds.len: + check schema.children[i].kind == kinds[i] + # and it is written back the way it was read + check toCddl(schema, pretty = true) == source + + dualTest "however many blank lines are written in a row stand for one node": + let schema = parseCddl("\n\n\na = int\n\n\n\nb = int\n\n\n") + check schema.children.len == 5 + let kinds = [nkEmpty, nkRule, nkEmpty, nkRule, nkEmpty] + for i in 0 ..< kinds.len: + check schema.children[i].kind == kinds[i] + # and what is written back is the one break each run stood for + check toCddl(schema, pretty = true) == "\na = int\n\nb = int\n\n" + + dualTest "a blank line inside a container is whitespace": + ## It has no line of its own among members that each get one, so there is + ## nowhere to write it back to. + let schema = parseCddl("a = {\n p: int,\n\n q: int,\n}\n") + check schema.children.len == 1 + check schema.children[0].body.children.len == 2 + + dualTest "ignoreComments drops the blank lines with the comments": + let schema = parseCddl("; about a\na = int\n\nb = int\n", ignoreComments = true) + check schema.children.len == 2 + check schema.children[0].kind == nkRule + check schema.children[1].kind == nkRule + +suite "Test CDDL validator": + dualTest "every tree the parser builds is valid": + for t in @testSpecCases & @testCases & @testBookCases: + for ignore in [false, true]: + let schema = parseCddl(t, ignoreComments = ignore) + if not schema.isValid: + checkpoint("INVALID: " & t & "\n\nTree:\n" & dumpTree(schema)) + fail() + + dualTest "a tree the parser could not have built is invalid": + template broken(body: untyped) = + block: + var n {.inject.} = parseCddl("a = {p: int, q: tstr}\n") + body + check not n.isValid + + broken: # an unfilled slot is no part of a finished tree + n.children[0].children[0].children[0] = CddlNode() + broken: # a rule without the body its parameters were declared for + n.children[0].children.setLen 0 + broken: # only rules and comments stand among a schema + n.children[0] = CddlNode(kind: nkEntry, children: @[CddlNode(kind: nkTypeRef)]) + broken: # a run of blank lines is folded into the one break it stands for + n.children.add CddlNode(kind: nkEmpty) + n.children.add CddlNode(kind: nkEmpty) + broken: # the key an entry says it has + n.children[0].children[0].children[0].children.setLen 1 + broken: # a leaf carries nothing + n.children[0].children[0].children[0].children[0].children.add( + CddlNode(kind: nkAny) + ) + broken: # an operator has two operands + n.children[0].children[0] = + CddlNode(kind: nkRange, children: @[CddlNode(kind: nkAny)]) + broken: # a choice of one would have been unwrapped + n.children[0].children[0] = + CddlNode(kind: nkUnion, children: @[CddlNode(kind: nkAny)]) + broken: # the grammar admits no tag but major 6 + n.children[0].children[0] = CddlNode( + kind: nkTagged, val: CddlNodeVal(major: 3), children: @[CddlNode(kind: nkAny)] + ) + broken: # a container holds members, not rules + n.children[0].children[0].children.add( + CddlNode(kind: nkRule, children: @[CddlNode(kind: nkAny)]) + ) + broken: # a choice stands for all the members, so it cannot sit among them + n.children[0].children[0].children.add CddlNode( + kind: nkGroupChoice, + children: @[CddlNode(kind: nkGroup), CddlNode(kind: nkGroup)], + ) + broken: # an entry is no type to be parenthesized + n.children[0].children[0] = CddlNode( + kind: nkParen, + children: @[CddlNode(kind: nkEntry, children: @[CddlNode(kind: nkAny)])], + ) + +suite "Test CDDL formatter": + dualTest "pretty breaks a container of more than one member": + let schema = parseCddl("long = {b: tstr, c: tstr}\n") + check toCddl(schema.children[0]) == "long = {b: tstr, c: tstr}" + check toCddl(schema.children[0], pretty = true) == + "long = {\n b: tstr,\n c: tstr,\n}" + + dualTest "pretty keeps a lone positional member on the line its container opened": + ## There is nothing for it to line up against, and what it holds still + ## breaks: the map opens and the array around it is left alone. + let schema = parseCddl( + "a = [* foo]\nb = [* {foo: int, bar: int}]\nc = [[(x)]]\n" & + "d = {\n ; why\n a\n}\n" + ) + check toCddl(schema.children[0], pretty = true) == "a = [* foo]" + check toCddl(schema.children[1], pretty = true) == + "b = [* {\n foo: int,\n bar: int,\n}]" + check toCddl(schema.children[2], pretty = true) == "c = [[(x)]]" + # a comment cannot share the line: it would swallow the closing delimiter + check toCddl(schema.children[3], pretty = true) == "d = {\n ; why\n a,\n}" + + dualTest "pretty breaks a container whose lone member has a key": + ## One field is still a field, and reads as one written on a line of its + ## own; only a positional member has nothing to line up against. + let schema = parseCddl("a = {key: value}\nb = {* tstr => any}\nc = [name: tstr]\n") + check toCddl(schema.children[0], pretty = true) == "a = {\n key: value,\n}" + check toCddl(schema.children[1], pretty = true) == "b = {\n * tstr => any,\n}" + check toCddl(schema.children[2], pretty = true) == "c = [\n name: tstr,\n]" + + dualTest "pretty indents a container for each one it sits inside": + let schema = parseCddl("t = {a: {b: [c: int, d: int], e: int}, f: int}\n") + check toCddl(schema.children[0], pretty = true) == + "t = {\n a: {\n b: [\n c: int,\n d: int,\n ],\n" & + " e: int,\n },\n f: int,\n}" + + dualTest "pretty leaves alone what holds no members": + # a range and a control operator are written the same either way + let schema = parseCddl("r = 0..10\nc = bstr .size 4\nv = 1\n") + for i in 0 ..< schema.children.len: + check toCddl(schema.children[i], pretty = true) == toCddl(schema.children[i]) + + dualTest "pretty gives a long type choice a line per variant": + ## Unlike a container, a choice only breaks once it would run off the line: + ## splitting up one of two variants reads worse than leaving it alone. + let short = parseCddl("k = a / b / c\n") + check toCddl(short.children[0], pretty = true) == "k = a / b / c" + + var long = "k = aaaaaaaaaa" + for i in 0 .. 5: + long &= " / bbbbbbbbbb" + let schema = parseCddl(long & "\n") + check toCddl(schema.children[0]).len > prettyWrap + check toCddl(schema.children[0], pretty = true) == + "k = aaaaaaaaaa /\n bbbbbbbbbb /\n bbbbbbbbbb /\n bbbbbbbbbb /\n" & + " bbbbbbbbbb /\n bbbbbbbbbb /\n bbbbbbbbbb" + + dualTest "pretty counts the indentation a choice sits at": + ## The same choice fits on its own line but not once it is indented far + ## enough in, so how deep it sits has to count against the wrap. + const choice = + "cccccccccc / dddddddddd / eeeeeeeeee / ffffffffff / gggggggggg / hhhhhhhhhh" + check choice.len <= prettyWrap + check toCddl(parseCddl("t = " & choice & "\n").children[0], pretty = true) == + "t = " & choice + + # two members apiece, so that every level of them is really indented + var src = "t = " + for i in 0 .. 3: + src &= "{a: " + src &= choice + for i in 0 .. 3: + src &= ", z: int}" + let schema = parseCddl(src & "\n") + let pretty = toCddl(schema.children[0], pretty = true) + check "cccccccccc /\n" in pretty # broken now, though the choice is the same + check parseCddl(pretty & "\n").children[0].toCddl() == schema.children[0].toCddl() + + dualTest "pretty printing writes comments back where they were": + let source = + "; about g\n" & "g = (\n" & " \"name\": tstr,\n" & " ; leading comment\n" & + " age: int, ; a bareword\n" & ")\n" + let schema = parseCddl(source) + check toCddl(schema, pretty = true) == source + # a comment forces its container open however well it would fit on a line + check toCddl(schema.children[1]) == "g = (\"name\": tstr, age: int)" + + dualTest "ignoreComments leaves a schema of nothing but its rules": + const source = + "; about person\n" & "person = {\n" & " ; about name\n" & + " name: tstr, ; ends the name line\n" & "}\n" & "; after the last rule\n" + let kept = parseCddl(source) + let ignored = parseCddl(source, ignoreComments = true) + check kept.children.len == 3 # the two comments outside the rule, and the rule + check ignored.children.len == 1 + check ignored.children[0].kind == nkRule + check ignored.children[0].body.children.len == 1 # no comment among the members + check ignored.children[0].body.children[0].kind == nkEntry + # the schema is otherwise the same one, and reads back the same way + check toCddl(kept.children[1]) == toCddl(ignored.children[0]) + check toCddl(ignored, pretty = true) == "person = {\n name: tstr,\n}\n" + + dualTest "a comment after the last rule is kept": + let schema = parseCddl("a = int\n; nothing follows this\n") + check schema.children.len == 2 + check schema.children[1].kind == nkComment + check toCddl(schema, pretty = true) == "a = int\n; nothing follows this\n" const schemaIssue36 = """ ; -- metadata -- @@ -768,19 +1470,37 @@ rt.get_readiness_response = { """ suite "Test CDDL parser issue 36": - staticTest "parse issue 36": + dualTest "parse issue 36": let schema = parseCddl(schemaIssue36) - var dump = "" - for r in schema: - dump.add "Rule: " & r.name & " [" & $r.kind & "]\n" - if r.genericParams.len > 0: - dump.add " genericParams: " & $r.genericParams & "\n" - if r.typeExpr.kind != fkUnset: - dump.add showType(r.typeExpr, 1) & "\n" - elif r.groupEntries.len > 0: - dump.add " groupEntries: " & $r.groupEntries.len & " field(s)\n" - const dumpFile = currentSourcePath.parentDir() / "test_cddl_parser_36_dump.txt" + let dump = toCddl(schema, pretty = true) + const dumpFile = + currentSourcePath.parentDir() / "test_cddl_parser_36_dump_pretty.txt" const dumpContent = staticRead(dumpFile) if dump.normalizeText() != dumpContent.normalizeText(): checkpoint(dump) fail() + +proc writeValue(w: var Cbor.Writer, value: Opt[uint64]) {.raises: [IOError].} = + if value.isSome: + w.writeValue(value.get()) + else: + w.writeValue(cborNull) + +proc readValue( + r: var Cbor.Reader, value: var Opt[uint64] +) {.raises: [IOError, SerializationError].} = + if r.parser.cborKind() == CborValueKind.Null: + discard r.readValue(CborSimpleValue) + value = Opt.none(uint64) + else: + value = Opt.some(r.readValue(uint64)) + +suite "Test CDDL CBOR encode/decode": + dualTest "roundtrip valid test cases": + for t in @testSpecCases & @testCases & @testBookCases: + let schema = parseCddl(t) + let encoded = Cbor.encode(schema) + let decoded = Cbor.decode(encoded, CddlNode) + if decoded != schema: + checkpoint(t) + fail() diff --git a/tests/test_cddl_parser_36_dump.txt b/tests/test_cddl_parser_36_dump.txt deleted file mode 100644 index 3d56546..0000000 --- a/tests/test_cddl_parser_36_dump.txt +++ /dev/null @@ -1,248 +0,0 @@ -Rule: _module [rkType] - Value("rt") -Rule: _version [rkType] - Array[ - [ocOne] - Value(1) - [ocOne] - Value(0) - ] -Rule: rt.state [rkType] - Union[ - Value("unloaded") - Value("loaded") - Value("ready") - Value("stopping") - Value("error") - ] -Rule: rt.mode [rkType] - Union[ - Value("direct") - Value("local-transport") - Value("remote-transport") - ] -Rule: rt.route_state [rkType] - Union[ - Value("establishing") - Value("ready") - Value("draining") - Value("revoked") - Value("failed") - Value("closed") - ] -Rule: rt.list_modules_request [rkType] - Map{ - } -Rule: rt.list_modules_response [rkType] - Map{ - [ocOne] key=modules(kkName) - Array[ - [ocZeroOrMore] - Map{ - [ocOne] key=module(kkName) - SimpleType(tstr) - [ocOptional] key=provider(kkName) - Map{ - [ocOptional] key=runtime_instance_id(kkName) - SimpleType(tstr) - [ocOne] key=provider(kkName) - SimpleType(tstr) - } - [ocOptional] key=remote(kkName) - Map{ - [ocOne] key=runtime(kkName) - Map{ - [ocOptional] key=runtime_instance_id(kkName) - SimpleType(tstr) - [ocOne] key=address(kkName) - Map{ - [ocOne] key=transport(kkName) - SimpleType(tstr) - [ocOptional] key=path(kkName) - SimpleType(tstr) - [ocOptional] key=host(kkName) - SimpleType(tstr) - [ocOptional] key=port(kkName) - SimpleType(uint) - [ocOptional] key=server_name(kkName) - SimpleType(tstr) - [ocOptional] key=alpn(kkName) - SimpleType(tstr) - } - } - [ocOptional] key=provider(kkName) - SimpleType(tstr) - [ocOptional] key=module(kkName) - SimpleType(tstr) - } - [ocOptional] key=instance(kkName) - SimpleType(tstr) - [ocOne] key=state(kkName) - SimpleType(rt.state) - [ocOne] key=mode(kkName) - SimpleType(rt.mode) - [ocOptional] key=schema_namespace(kkName) - SimpleType(tstr) - [ocOptional] key=schema(kkName) - Map{ - [ocOne] key=commitment_model(kkName) - SimpleType(tstr) - [ocOne] key=schema_root(kkName) - SimpleType(bstr) - [ocOne] key=hash_profile(kkName) - SimpleType(tstr) - [ocOne] key=hash_suite(kkName) - SimpleType(tstr) - } - [ocOptional] key=reason(kkName) - SimpleType(tstr) - } - ] - } -Rule: rt.list_routes_request [rkType] - Map{ - [ocOptional] key=module(kkName) - SimpleType(tstr) - [ocOptional] key=provider(kkName) - Map{ - [ocOptional] key=runtime_instance_id(kkName) - SimpleType(tstr) - [ocOne] key=provider(kkName) - SimpleType(tstr) - } - } -Rule: rt.list_routes_response [rkType] - Map{ - [ocOne] key=routes(kkName) - Array[ - [ocZeroOrMore] - Map{ - [ocOne] key=route(kkName) - SimpleType(tstr) - [ocOne] key=caller_runtime(kkName) - SimpleType(tstr) - [ocOne] key=target_provider(kkName) - Map{ - [ocOptional] key=runtime_instance_id(kkName) - SimpleType(tstr) - [ocOne] key=provider(kkName) - SimpleType(tstr) - } - [ocOne] key=module(kkName) - SimpleType(tstr) - [ocOptional] key=instance(kkName) - SimpleType(tstr) - [ocOptional] key=schema_namespace(kkName) - SimpleType(tstr) - [ocOptional] key=schema(kkName) - Map{ - [ocOne] key=commitment_model(kkName) - SimpleType(tstr) - [ocOne] key=schema_root(kkName) - SimpleType(bstr) - [ocOne] key=hash_profile(kkName) - SimpleType(tstr) - [ocOne] key=hash_suite(kkName) - SimpleType(tstr) - } - [ocOne] key=state(kkName) - SimpleType(rt.route_state) - [ocOne] key=invocation(kkName) - Map{ - [ocOne] key=kind(kkName) - SimpleType(rt.mode) - [ocOne] key=descriptor_kind(kkName) - SimpleType(tstr) - [ocOptional] key=descriptor(kkName) - SimpleType(bstr) - } - [ocOptional] key=authority(kkName) - Map{ - [ocOptional] key=authority_provider(kkName) - Map{ - [ocOptional] key=runtime_instance_id(kkName) - SimpleType(tstr) - [ocOne] key=provider(kkName) - SimpleType(tstr) - } - [ocOptional] key=authority_ref(kkName) - SimpleType(tstr) - [ocOptional] key=expires_at(kkName) - SimpleType(uint) - [ocOptional] key=audit_ref(kkName) - SimpleType(tstr) - } - [ocOptional] key=failure(kkName) - Map{ - [ocOne] key=code(kkName) - SimpleType(tstr) - [ocOptional] key=message(kkName) - SimpleType(tstr) - } - } - ] - } -Rule: rt.revoke_route_request [rkType] - Map{ - [ocOne] key=route(kkName) - SimpleType(tstr) - [ocOptional] key=reason(kkName) - SimpleType(tstr) - } -Rule: rt.revoke_route_response [rkType] - Map{ - [ocOne] key=route(kkName) - SimpleType(tstr) - [ocOne] key=state(kkName) - SimpleType(rt.route_state) - } -Rule: rt.start_module_request [rkType] - Map{ - [ocOne] key=module(kkName) - SimpleType(tstr) - [ocOptional] key=instance(kkName) - SimpleType(tstr) - } -Rule: rt.start_module_response [rkType] - Map{ - [ocOne] key=module(kkName) - SimpleType(tstr) - [ocOne] key=instance(kkName) - SimpleType(tstr) - [ocOne] key=state(kkName) - SimpleType(rt.state) - } -Rule: rt.stop_module_request [rkType] - Map{ - [ocOne] key=module(kkName) - SimpleType(tstr) - [ocOptional] key=instance(kkName) - SimpleType(tstr) - } -Rule: rt.stop_module_response [rkType] - Map{ - [ocOne] key=module(kkName) - SimpleType(tstr) - [ocOptional] key=instance(kkName) - SimpleType(tstr) - [ocOne] key=state(kkName) - SimpleType(rt.state) - } -Rule: rt.get_readiness_request [rkType] - Map{ - [ocOne] key=module(kkName) - SimpleType(tstr) - [ocOptional] key=instance(kkName) - SimpleType(tstr) - } -Rule: rt.get_readiness_response [rkType] - Map{ - [ocOne] key=module(kkName) - SimpleType(tstr) - [ocOptional] key=instance(kkName) - SimpleType(tstr) - [ocOne] key=state(kkName) - SimpleType(rt.state) - [ocOptional] key=reason(kkName) - SimpleType(tstr) - } diff --git a/tests/test_cddl_parser_36_dump_pretty.txt b/tests/test_cddl_parser_36_dump_pretty.txt new file mode 100644 index 0000000..8736e2d --- /dev/null +++ b/tests/test_cddl_parser_36_dump_pretty.txt @@ -0,0 +1,142 @@ +; -- metadata -- +_module = "rt" +_version = [ + 1, + 0, +] + +; -- types -- +rt.state = "unloaded" / "loaded" / "ready" / "stopping" / "error" +rt.mode = "direct" / "local-transport" / "remote-transport" +rt.route_state = "establishing" / "ready" / "draining" / "revoked" / "failed" / "closed" + +; -- method definitions -- + +; list_modules +rt.list_modules_request = {} +rt.list_modules_response = { + modules: [* { + module: tstr, + ? provider: { + ? runtime_instance_id: tstr, + provider: tstr, + }, + ? remote: { + runtime: { + ? runtime_instance_id: tstr, + address: { + transport: tstr, + ? path: tstr, + ? host: tstr, + ? port: uint, + ? server_name: tstr, + ? alpn: tstr, + }, + }, + ? provider: tstr, + ? module: tstr, + }, + ? instance: tstr, + state: rt.state, + mode: rt.mode, + ? schema_namespace: tstr, + ? schema: { + commitment_model: tstr, + schema_root: bstr, + hash_profile: tstr, + hash_suite: tstr, + }, + ? reason: tstr, + }], +} + +; list_routes +rt.list_routes_request = { + ? module: tstr, + ? provider: { + ? runtime_instance_id: tstr, + provider: tstr, + }, +} +rt.list_routes_response = { + routes: [* { + route: tstr, + caller_runtime: tstr, + target_provider: { + ? runtime_instance_id: tstr, + provider: tstr, + }, + module: tstr, + ? instance: tstr, + ? schema_namespace: tstr, + ? schema: { + commitment_model: tstr, + schema_root: bstr, + hash_profile: tstr, + hash_suite: tstr, + }, + state: rt.route_state, + invocation: { + kind: rt.mode, + descriptor_kind: tstr, + ? descriptor: bstr, + }, + ? authority: { + ? authority_provider: { + ? runtime_instance_id: tstr, + provider: tstr, + }, + ? authority_ref: tstr, + ? expires_at: uint, + ? audit_ref: tstr, + }, + ? failure: { + code: tstr, + ? message: tstr, + }, + }], +} + +; revoke_route +rt.revoke_route_request = { + route: tstr, + ? reason: tstr, +} +rt.revoke_route_response = { + route: tstr, + state: rt.route_state, +} + +; start_module +rt.start_module_request = { + module: tstr, + ? instance: tstr, +} +rt.start_module_response = { + module: tstr, + instance: tstr, + state: rt.state, +} + +; stop_module +rt.stop_module_request = { + module: tstr, + ? instance: tstr, +} +rt.stop_module_response = { + module: tstr, + ? instance: tstr, + state: rt.state, +} + +; get_readiness +rt.get_readiness_request = { + module: tstr, + ? instance: tstr, +} +rt.get_readiness_response = { + module: tstr, + ? instance: tstr, + state: rt.state, + ? reason: tstr, +} diff --git a/tests/test_cddl_parser_dump.txt b/tests/test_cddl_parser_dump.txt index d3d1fa2..bff2f9e 100644 --- a/tests/test_cddl_parser_dump.txt +++ b/tests/test_cddl_parser_dump.txt @@ -1,478 +1,1941 @@ -Rule: foo [rkType] - SimpleType(bar) -Rule: foo [rkType] - Union[ - SimpleType(uint) - SimpleType(tstr) - SimpleType(bstr) - ] -Rule: small [rkType] - Generic(..<0,100>) -Rule: name [rkType] - Generic(..<1,..<1,64>>) -Rule: my-type [rkType] - SimpleType(nil) -Rule: tagged [rkType] - Tagged(6.1) - SimpleType(tstr) -Rule: any-cbor [rkType] +Rule person [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(age) + TypeRef(int) + Entry [ocOne] key(skColon) + TypeRef(name) + TypeRef(tstr) + Entry [ocOne] key(skColon) + TypeRef(employer) + TypeRef(tstr) + ; person = {age: int, name: tstr, employer: tstr} +Rule pii [rkGroup] + Entry [ocOne] + Group + Entry [ocOne] key(skColon) + TypeRef(age) + TypeRef(int) + Entry [ocOne] key(skColon) + TypeRef(name) + TypeRef(tstr) + Entry [ocOne] key(skColon) + TypeRef(employer) + TypeRef(tstr) + ; pii = (age: int, name: tstr, employer: tstr) +Rule person [rkType] + Map + Entry [ocOne] + TypeRef(pii) + ; person = {pii} +Rule person [rkType] + Map + Entry [ocOne] + Group + Entry [ocOne] key(skColon) + TypeRef(age) + TypeRef(int) + Entry [ocOne] key(skColon) + TypeRef(name) + TypeRef(tstr) + Entry [ocOne] key(skColon) + TypeRef(employer) + TypeRef(tstr) + ; person = {(age: int, name: tstr, employer: tstr)} +Rule person [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(age) + TypeRef(int) + Entry [ocOne] key(skColon) + TypeRef(name) + TypeRef(tstr) + Entry [ocOne] key(skColon) + TypeRef(employer) + TypeRef(tstr) + ; person = {age: int, name: tstr, employer: tstr} +Empty +Rule dog [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(age) + TypeRef(int) + Entry [ocOne] key(skColon) + TypeRef(name) + TypeRef(tstr) + Entry [ocOne] key(skColon) + TypeRef(leash-length) + TypeRef(float) + ; dog = {age: int, name: tstr, leash-length: float} +Rule person [rkType] + Map + Entry [ocOne] + TypeRef(identity) + Entry [ocOne] key(skColon) + TypeRef(employer) + TypeRef(tstr) + ; person = {identity, employer: tstr} +Empty +Rule dog [rkType] + Map + Entry [ocOne] + TypeRef(identity) + Entry [ocOne] key(skColon) + TypeRef(leash-length) + TypeRef(float) + ; dog = {identity, leash-length: float} +Empty +Rule identity [rkGroup] + Entry [ocOne] + Group + Entry [ocOne] key(skColon) + TypeRef(age) + TypeRef(int) + Entry [ocOne] key(skColon) + TypeRef(name) + TypeRef(tstr) + ; identity = (age: int, name: tstr) +Rule attire [rkType] + Union + Value("bow tie") + Value("necktie") + Value("Internet attire") + ; attire = "bow tie" / "necktie" / "Internet attire" +Rule protocol [rkType] + Union + Value(6) + Value(17) + ; protocol = 6 / 17 +Rule address [rkType] + Map + Entry [ocOne] + TypeRef(delivery) + ; address = {delivery} +Empty +Rule delivery [rkGroup] + Entry [ocOne] + Group + GroupChoice + Group + Entry [ocOne] key(skColon) + TypeRef(street) + TypeRef(tstr) + Entry [ocOptional] key(skColon) + TypeRef(number) + TypeRef(uint) + Entry [ocOne] + TypeRef(city) + Group + Entry [ocOne] key(skColon) + TypeRef(po-box) + TypeRef(uint) + Entry [ocOne] + TypeRef(city) + Group + Entry [ocOne] key(skColon) + TypeRef(per-pickup) + TypeRef(true) + ; delivery = (street: tstr, ? number: uint, city // po-box: uint, city // per-pickup: true) +Empty +Rule city [rkGroup] + Entry [ocOne] + Group + Entry [ocOne] key(skColon) + TypeRef(name) + TypeRef(tstr) + Entry [ocOne] key(skColon) + TypeRef(zip-code) + TypeRef(uint) + ; city = (name: tstr, zip-code: uint) +Rule attire [rkTypeExt] + Value("swimwear") + ; attire /= "swimwear" +Empty +Rule delivery [rkGroupExt] + Entry [ocOne] + Group + Entry [ocOne] key(skColon) + TypeRef(lat) + TypeRef(float) + Entry [ocOne] key(skColon) + TypeRef(long) + TypeRef(float) + Entry [ocOne] key(skColon) + TypeRef(drone-type) + TypeRef(tstr) + ; delivery //= (lat: float, long: float, drone-type: tstr) +Rule device-address [rkType] + TypeRef(byte) + ; device-address = byte +Rule max-byte [rkType] + Value(255) + ; max-byte = 255 +Rule byte [rkType] + Range(..) + Value(0) + TypeRef(max-byte) + ; byte = 0..max-byte +CommentInline(; inclusive range) +Rule first-non-byte [rkType] + Value(256) + ; first-non-byte = 256 +Rule byte1 [rkType] + Range(...) + Value(0) + TypeRef(first-non-byte) + ; byte1 = 0...first-non-byte +CommentInline(; byte1 is equivalent to byte) +Rule int-range [rkType] + Range(..) + Value(0) + Value(10) + ; int-range = 0..10 +CommentInline(; only integers match) +Rule float-range [rkType] + Range(..) + Value(0.0) + Value(10.0) + ; float-range = 0.0..10.0 +CommentInline(; only floats match) +Rule BAD-range1 [rkType] + Range(..) + Value(0) + Value(10.0) + ; BAD-range1 = 0..10.0 +CommentInline(; NOT DEFINED) +Rule BAD-range2 [rkType] + Range(..) + Value(0.0) + Value(10) + ; BAD-range2 = 0.0..10 +CommentInline(; NOT DEFINED) +Rule numeric-range [rkType] + Union + TypeRef(int-range) + TypeRef(float-range) + ; numeric-range = int-range / float-range +Rule terminal-color [rkType] + Enum + TypeRef(basecolors) + ; terminal-color = &basecolors +Rule basecolors [rkGroup] + Entry [ocOne] + Group + Entry [ocOne] key(skColon) + TypeRef(black) + Value(0) + Entry [ocOne] key(skColon) + TypeRef(red) + Value(1) + Entry [ocOne] key(skColon) + TypeRef(green) + Value(2) + Entry [ocOne] key(skColon) + TypeRef(yellow) + Value(3) + Entry [ocOne] key(skColon) + TypeRef(blue) + Value(4) + Entry [ocOne] key(skColon) + TypeRef(magenta) + Value(5) + Entry [ocOne] key(skColon) + TypeRef(cyan) + Value(6) + Entry [ocOne] key(skColon) + TypeRef(white) + Value(7) + ; basecolors = (black: 0, red: 1, green: 2, yellow: 3, blue: 4, magenta: 5, cyan: 6, white: 7) +Rule extended-color [rkType] + Enum + Group + Entry [ocOne] + TypeRef(basecolors) + Entry [ocOne] key(skColon) + TypeRef(orange) + Value(8) + Entry [ocOne] key(skColon) + TypeRef(pink) + Value(9) + Entry [ocOne] key(skColon) + TypeRef(purple) + Value(10) + Entry [ocOne] key(skColon) + TypeRef(brown) + Value(11) + ; extended-color = &(basecolors, orange: 8, pink: 9, purple: 10, brown: 11) +Rule my_breakfast [rkType] + Tagged(#6.55799) + TypeRef(breakfast) + ; my_breakfast = #6.55799(breakfast) +CommentInline(; cbor-any is too general!) +Rule breakfast [rkType] + Union + TypeRef(cereal) + TypeRef(porridge) + ; breakfast = cereal / porridge +Rule cereal [rkType] + Tagged(#6.998) + TypeRef(tstr) + ; cereal = #6.998(tstr) +Rule porridge [rkType] + Tagged(#6.999) + Array + Entry [ocOne] + TypeRef(liquid) + Entry [ocOne] + TypeRef(solid) + ; porridge = #6.999([liquid, solid]) +Rule liquid [rkType] + Union + TypeRef(milk) + TypeRef(water) + ; liquid = milk / water +Rule milk [rkType] + Value(0) + ; milk = 0 +Rule water [rkType] + Value(1) + ; water = 1 +Rule solid [rkType] + TypeRef(tstr) + ; solid = tstr +Comment(; This is a comment) +Rule person [rkType] + Map + Entry [ocOne] + TypeRef(g) + ; person = {g} +Empty +Rule g [rkGroup] + Entry [ocOne] + Group + Entry [ocOne] key(skColon) + Value("name") + TypeRef(tstr) + Entry [ocOne] key(skColon) + TypeRef(age) + TypeRef(int) + CommentInline(; "age" is a bareword) + ; g = ("name": tstr, age: int) +Rule apartment [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(kitchen) + TypeRef(size) + Entry [ocZeroOrMore] key(skColon) + TypeRef(bedroom) + TypeRef(size) + ; apartment = {kitchen: size, * bedroom: size} +Rule size [rkType] + TypeRef(float) + ; size = float +CommentInline(; in m2) +Rule unlimited-people [rkType] + Array + Entry [ocZeroOrMore] + TypeRef(person) + ; unlimited-people = [* person] +Rule one-or-two-people [rkType] + Array + Entry [ocRange] 1*2 + TypeRef(person) + ; one-or-two-people = [1*2 person] +Rule at-least-two-people [rkType] + Array + Entry [ocRange] 2* + TypeRef(person) + ; at-least-two-people = [2* person] +Rule person [rkGroup] + Entry [ocOne] + Group + Entry [ocOne] key(skColon) + TypeRef(name) + TypeRef(tstr) + Entry [ocOne] key(skColon) + TypeRef(age) + TypeRef(uint) + ; person = (name: tstr, age: uint) +Rule Geography [rkType] + Array + Entry [ocOne] key(skColon) + TypeRef(city) + TypeRef(tstr) + Entry [ocOne] key(skColon) + TypeRef(gpsCoordinates) + TypeRef(GpsCoordinates) + ; Geography = [city: tstr, gpsCoordinates: GpsCoordinates] +Empty +Rule GpsCoordinates [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(longitude) + TypeRef(uint) + CommentInline(; degrees, scaled by 10^7) + Entry [ocOne] key(skColon) + TypeRef(latitude) + TypeRef(uint) + CommentInline(; degrees, scaled by 10^7) + ; GpsCoordinates = {longitude: uint, latitude: uint} +Rule located-samples [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(sample-point) + TypeRef(int) + Entry [ocOne] key(skColon) + TypeRef(samples) + Array + Entry [ocOneOrMore] + TypeRef(float) + ; located-samples = {sample-point: int, samples: [+ float]} +Rule located-samples [rkType] + Map + Entry [ocOne] key(skArrow) + Value("sample-point") + TypeRef(int) + Entry [ocOne] key(skArrow) + Value("samples") + Array + Entry [ocOneOrMore] + TypeRef(float) + ; located-samples = {"sample-point" => int, "samples" => [+ float]} +Rule located-samples [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(sample-point) + TypeRef(int) + Entry [ocOne] key(skColon) + TypeRef(samples) + Array + Entry [ocOneOrMore] + TypeRef(float) + Entry [ocZeroOrMore] key(skArrow) + TypeRef(equipment-type) + TypeRef(equipment-tolerances) + ; located-samples = {sample-point: int, samples: [+ float], * equipment-type => equipment-tolerances} +Rule equipment-type [rkType] + Array + Entry [ocOne] key(skColon) + TypeRef(name) + TypeRef(tstr) + Entry [ocOne] key(skColon) + TypeRef(manufacturer) + TypeRef(tstr) + ; equipment-type = [name: tstr, manufacturer: tstr] +Rule equipment-tolerances [rkType] + Array + Entry [ocOneOrMore] + Array + Entry [ocOne] + TypeRef(float) + Entry [ocOne] + TypeRef(float) + ; equipment-tolerances = [+ [float, float]] +Rule PersonalData [rkType] + Map + Entry [ocOptional] key(skColon) + TypeRef(displayName) + TypeRef(tstr) + Entry [ocOne] + TypeRef(NameComponents) + Entry [ocOptional] key(skColon) + TypeRef(age) + TypeRef(uint) + ; PersonalData = {? displayName: tstr, NameComponents, ? age: uint} +Empty +Rule NameComponents [rkGroup] + Entry [ocOne] + Group + Entry [ocOptional] key(skColon) + TypeRef(firstName) + TypeRef(tstr) + Entry [ocOptional] key(skColon) + TypeRef(familyName) + TypeRef(tstr) + ; NameComponents = (? firstName: tstr, ? familyName: tstr) +Rule PersonalData [rkType] + Map + Entry [ocOptional] key(skColon) + TypeRef(displayName) + TypeRef(tstr) + Entry [ocOne] + TypeRef(NameComponents) + Entry [ocOptional] key(skColon) + TypeRef(age) + TypeRef(uint) + Entry [ocZeroOrMore] key(skArrow) + TypeRef(tstr) + TypeRef(any) + ; PersonalData = {? displayName: tstr, NameComponents, ? age: uint, * tstr => any} +Empty +Rule NameComponents [rkGroup] + Entry [ocOne] + Group + Entry [ocOptional] key(skColon) + TypeRef(firstName) + TypeRef(tstr) + Entry [ocOptional] key(skColon) + TypeRef(familyName) + TypeRef(tstr) + ; NameComponents = (? firstName: tstr, ? familyName: tstr) +Rule square-roots [rkType] + Map + Entry [ocZeroOrMore] key(skArrow) + TypeRef(x) + TypeRef(y) + ; square-roots = {* x => y} +Rule x [rkType] + TypeRef(int) + ; x = int +Rule y [rkType] + TypeRef(float) + ; y = float +Rule tostring [rkType] + Map + Entry [ocZeroOrMore] key(skArrow) + TypeRef(mynumber) + TypeRef(tstr) + ; tostring = {* mynumber => tstr} +Rule mynumber [rkType] + Union + TypeRef(int) + TypeRef(float) + ; mynumber = int / float +Rule labeled-values [rkType] + Map + Entry [ocOptional] key(skColon) + TypeRef(fritz) + TypeRef(number) + Entry [ocZeroOrMore] key(skArrow) + TypeRef(label) + TypeRef(value) + ; labeled-values = {? fritz: number, * label => value} +Rule label [rkType] + TypeRef(text) + ; label = text +Rule value [rkType] + TypeRef(number) + ; value = number +Rule do-not-do-this [rkType] + Map + Entry [ocOne] key(skArrow) + TypeRef(int) + TypeRef(int) + Entry [ocOne] key(skArrow) + TypeRef(int) + Value(6) + ; do-not-do-this = {int => int, int => 6} +Rule extensible-map-example [rkType] + Map + Entry [ocOptional] key(skArrow) + Value("optional-key") + TypeRef(int) + Entry [ocZeroOrMore] key(skArrow) + TypeRef(tstr) + TypeRef(any) + ; extensible-map-example = {? "optional-key" => int, * tstr => any} +Rule extensible-map-example [rkType] + Map + Entry [ocOptional] key(skArrowCut) + Value("optional-key") + TypeRef(int) + Entry [ocZeroOrMore] key(skArrow) + TypeRef(tstr) + TypeRef(any) + ; extensible-map-example = {? "optional-key" ^ => int, * tstr => any} +Rule extensible-map-example [rkType] + Map + Entry [ocOptional] key(skColon) + Value("optional-key") + TypeRef(int) + Entry [ocZeroOrMore] key(skArrow) + TypeRef(tstr) + TypeRef(any) + ; extensible-map-example = {? "optional-key": int, * tstr => any} +Rule extensible-map-example [rkType] + Map + Entry [ocOptional] key(skColon) + TypeRef(optional-key) + TypeRef(int) + Entry [ocZeroOrMore] key(skArrow) + TypeRef(tstr) + TypeRef(any) + ; extensible-map-example = {? optional-key: int, * tstr => any} +Rule buuid [rkType] + Tagged(#6.37) + TypeRef(bstr) + ; buuid = #6.37(bstr) +Rule my_uri [rkType] + Union + Tagged(#6.32) + TypeRef(tstr) + TypeRef(tstr) + ; my_uri = #6.32(tstr) / tstr +Rule basic-header-group [rkGroup] + Entry [ocOne] + Group + Entry [ocOne] key(skColon) + TypeRef(field1) + TypeRef(int) + Entry [ocOne] key(skColon) + TypeRef(field2) + TypeRef(text) + ; basic-header-group = (field1: int, field2: text) +Empty +Rule basic-header [rkType] + Array + Entry [ocOne] + TypeRef(basic-header-group) + ; basic-header = [basic-header-group] +Empty +Rule advanced-header [rkType] + Array + Entry [ocOne] + TypeRef(basic-header-group) + Entry [ocOne] key(skColon) + TypeRef(field3) + TypeRef(bytes) + Entry [ocOne] key(skColon) + TypeRef(field4) + TypeRef(number) + CommentInline(; as in the tagged type "time") + ; advanced-header = [basic-header-group, field3: bytes, field4: number] +Rule basic-header [rkType] + Array + Entry [ocOne] key(skColon) + TypeRef(field1) + TypeRef(int) + Entry [ocOne] key(skColon) + TypeRef(field2) + TypeRef(text) + ; basic-header = [field1: int, field2: text] +Empty +Rule advanced-header [rkType] + Array + Entry [ocOne] + Unwrap + TypeRef(basic-header) + Entry [ocOne] key(skColon) + TypeRef(field3) + TypeRef(bytes) + Entry [ocOne] key(skColon) + TypeRef(field4) + Unwrap + TypeRef(time) + ; advanced-header = [~basic-header, field3: bytes, field4: ~time] +Rule full-address [rkType] + Array + Entry [ocOne] + Array + Entry [ocOneOrMore] + TypeRef(label) + Entry [ocOne] + TypeRef(ip4) + Entry [ocOne] + TypeRef(ip6) + ; full-address = [[+ label], ip4, ip6] +Rule ip4 [rkType] + Control(.size) + TypeRef(bstr) + Value(4) + ; ip4 = bstr .size 4 +Rule ip6 [rkType] + Control(.size) + TypeRef(bstr) + Value(16) + ; ip6 = bstr .size 16 +Rule label [rkType] + Control(.size) + TypeRef(bstr) + Paren + Range(..) + Value(1) + Value(63) + ; label = bstr .size (1..63) +Rule audio_sample [rkType] + Control(.size) + TypeRef(uint) + Value(3) + ; audio_sample = uint .size 3 +CommentInline(; 24-bit, equivalent to 0...16777216) +Rule tcpflagbytes [rkType] + Control(.bits) + TypeRef(bstr) + TypeRef(flags) + ; tcpflagbytes = bstr .bits flags +Rule flags [rkType] + Union + Enum + Group + Entry [ocOne] key(skColon) + TypeRef(fin) + Value(8) + Entry [ocOne] key(skColon) + TypeRef(syn) + Value(9) + Entry [ocOne] key(skColon) + TypeRef(rst) + Value(10) + Entry [ocOne] key(skColon) + TypeRef(psh) + Value(11) + Entry [ocOne] key(skColon) + TypeRef(ack) + Value(12) + Entry [ocOne] key(skColon) + TypeRef(urg) + Value(13) + Entry [ocOne] key(skColon) + TypeRef(ece) + Value(14) + Entry [ocOne] key(skColon) + TypeRef(cwr) + Value(15) + Entry [ocOne] key(skColon) + TypeRef(ns) + Value(0) + Paren + Range(..) + Value(4) + Value(7) + ; flags = &(fin: 8, syn: 9, rst: 10, psh: 11, ack: 12, urg: 13, ece: 14, cwr: 15, ns: 0) / (4..7) +CommentInline(; data offset bits) +Empty +Rule rwxbits [rkType] + Control(.bits) + TypeRef(uint) + TypeRef(rwx) + ; rwxbits = uint .bits rwx +Rule rwx [rkType] + Enum + Group + Entry [ocOne] key(skColon) + TypeRef(r) + Value(2) + Entry [ocOne] key(skColon) + TypeRef(w) + Value(1) + Entry [ocOne] key(skColon) + TypeRef(x) + Value(0) + ; rwx = &(r: 2, w: 1, x: 0) +Rule nai [rkType] + Control(.regexp) + TypeRef(tstr) + Value("[A-Za-z0-9]+@[A-Za-z0-9]+(\\.[A-Za-z0-9]+)+") + ; nai = tstr .regexp "[A-Za-z0-9]+@[A-Za-z0-9]+(\\.[A-Za-z0-9]+)+" +Rule message [rkType] + Control(.within) + TypeRef($message) + TypeRef(message-structure) + ; message = $message .within message-structure +Rule message-structure [rkType] + Array + Entry [ocOne] + TypeRef(message_type) + Entry [ocZeroOrMore] + TypeRef(message_option) + ; message-structure = [message_type, * message_option] +Rule message_type [rkType] + Range(..) + Value(0) + Value(255) + ; message_type = 0..255 +Rule message_option [rkType] + TypeRef(any) + ; message_option = any +Empty +Rule $message [rkTypeExt] + Array + Entry [ocOne] + Value(3) + Entry [ocOne] key(skColon) + TypeRef(dough) + TypeRef(text) + Entry [ocOne] key(skColon) + TypeRef(topping) + Array + Entry [ocZeroOrMore] + TypeRef(text) + ; $message /= [3, dough: text, topping: [* text]] +Rule $message [rkTypeExt] + Array + Entry [ocOne] + Value(4) + Entry [ocOne] key(skColon) + TypeRef(noodles) + TypeRef(text) + Entry [ocOne] key(skColon) + TypeRef(sauce) + TypeRef(text) + Entry [ocOne] key(skColon) + TypeRef(parmesan) + TypeRef(bool) + ; $message /= [4, noodles: text, sauce: text, parmesan: bool] +Rule speed [rkType] + Control(.ge) + TypeRef(number) + Value(0) + ; speed = number .ge 0 +CommentInline(; unit: m/s) +Rule timer [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(time) + TypeRef(uint) + Entry [ocOptional] key(skColon) + TypeRef(displayed-step) + Control(.default) + Paren + Control(.gt) + TypeRef(number) + Value(0) + Value(1) + ; timer = {time: uint, ? displayed-step: (number .gt 0) .default 1} +Rule tcp-header [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(seq) + TypeRef(uint) + Entry [ocOne] key(skColon) + TypeRef(ack) + TypeRef(uint) + Entry [ocZeroOrMore] + TypeRef($$tcp-option) + ; tcp-header = {seq: uint, ack: uint, * $$tcp-option} +Empty +Comment(; later, in a different file) +Empty +Rule $$tcp-option [rkGroupExt] + Entry [ocOne] + Group + Entry [ocOne] key(skColon) + TypeRef(sack) + Array + Entry [ocOneOrMore] + Group + Entry [ocOne] key(skColon) + TypeRef(left) + TypeRef(uint) + Entry [ocOne] key(skColon) + TypeRef(right) + TypeRef(uint) + ; $$tcp-option //= (sack: [+ (left: uint, right: uint)]) +Empty +Comment(; and, maybe in another file) +Empty +Rule $$tcp-option [rkGroupExt] + Entry [ocOne] + Group + Entry [ocOne] key(skColon) + TypeRef(sack-permitted) + TypeRef(true) + ; $$tcp-option //= (sack-permitted: true) +Rule PersonalData [rkType] + Map + Entry [ocOptional] key(skColon) + TypeRef(displayName) + TypeRef(tstr) + Entry [ocOne] + TypeRef(NameComponents) + Entry [ocOptional] key(skColon) + TypeRef(age) + TypeRef(uint) + Entry [ocZeroOrMore] + TypeRef($$personaldata-extensions) + ; PersonalData = {? displayName: tstr, NameComponents, ? age: uint, * $$personaldata-extensions} +Empty +Rule NameComponents [rkGroup] + Entry [ocOne] + Group + Entry [ocOptional] key(skColon) + TypeRef(firstName) + TypeRef(tstr) + Entry [ocOptional] key(skColon) + TypeRef(familyName) + TypeRef(tstr) + ; NameComponents = (? firstName: tstr, ? familyName: tstr) +Empty +Comment(; The above already works as is.) +Comment(; But then, we can add later:) +Empty +Rule $$personaldata-extensions [rkGroupExt] + Entry [ocOne] + Group + Entry [ocOne] key(skColon) + TypeRef(favorite-salsa) + TypeRef(tstr) + ; $$personaldata-extensions //= (favorite-salsa: tstr) +Empty +Comment(; and again, somewhere else:) +Empty +Rule $$personaldata-extensions [rkGroupExt] + Entry [ocOne] + Group + Entry [ocOne] key(skColon) + TypeRef(shoesize) + TypeRef(uint) + ; $$personaldata-extensions //= (shoesize: uint) +Rule messages [rkType] + Union + Generic(message) + Value("reboot") + Value("now") + Generic(message) + Value("sleep") + Range(..) + Value(1) + Value(100) + ; messages = message<"reboot", "now"> / message<"sleep", 1..100> +Rule message [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(type) + TypeRef(t) + Entry [ocOne] key(skColon) + TypeRef(value) + TypeRef(v) + ; message = {type: t, value: v} +Rule t [rkType] + Array + Entry [ocOne] + TypeRef(group1) + ; t = [group1] +Rule group1 [rkGroup] + Entry [ocOne] + Group + GroupChoice + Group + Entry [ocOne] + Union + TypeRef(a) + TypeRef(b) + Group + Entry [ocOne] + Union + TypeRef(c) + TypeRef(d) + ; group1 = (a / b // c / d) +Rule a [rkType] + Value(1) + ; a = 1 +Rule b [rkType] + Value(2) + ; b = 2 +Rule c [rkType] + Value(3) + ; c = 3 +Rule d [rkType] + Value(4) + ; d = 4 +Rule t [rkType] + Map + Entry [ocOne] + TypeRef(group2) + ; t = {group2} +Rule group2 [rkGroup] + Entry [ocOne] + Group + GroupChoice + Group + Entry [ocOptional] key(skColon) + TypeRef(ab) + Union + TypeRef(a) + TypeRef(b) + Group + Entry [ocOne] key(skColon) + TypeRef(cd) + Union + TypeRef(c) + TypeRef(d) + ; group2 = (? ab: a / b // cd: c / d) +Rule a [rkType] + Value(1) + ; a = 1 +Rule b [rkType] + Value(2) + ; b = 2 +Rule c [rkType] + Value(3) + ; c = 3 +Rule d [rkType] + Value(4) + ; d = 4 +Rule t [rkType] + Array + Entry [ocOne] + TypeRef(group3) + ; t = [group3] +Rule group3 [rkGroup] + Entry [ocOne] + Group + Entry [ocOneOrMore] + Union + TypeRef(a) + TypeRef(b) + TypeRef(c) + ; group3 = (+ a / b / c) +Rule a [rkType] + Value(1) + ; a = 1 +Rule b [rkType] + Value(2) + ; b = 2 +Rule c [rkType] + Value(3) + ; c = 3 +Rule t [rkType] + Array + Entry [ocOne] + TypeRef(group4) + ; t = [group4] +Rule group4 [rkGroup] + Entry [ocOne] + Group + GroupChoice + Group + Entry [ocOneOrMore] + TypeRef(a) + Group + Entry [ocOne] + Union + TypeRef(b) + TypeRef(c) + ; group4 = (+ a // b / c) +Rule a [rkType] + Value(1) + ; a = 1 +Rule b [rkType] + Value(2) + ; b = 2 +Rule c [rkType] + Value(3) + ; c = 3 +Rule t [rkType] + Array + Entry [ocOne] + TypeRef(group4a) + ; t = [group4a] +Rule group4a [rkGroup] + Entry [ocOne] + Group + GroupChoice + Group + Entry [ocOne] + Group + Entry [ocOneOrMore] + TypeRef(a) + Group + Entry [ocOne] + Paren + Union + TypeRef(b) + TypeRef(c) + ; group4a = ((+ a) // (b / c)) +Rule a [rkType] + Value(1) + ; a = 1 +Rule b [rkType] + Value(2) + ; b = 2 +Rule c [rkType] + Value(3) + ; c = 3 +Rule foo [rkType] + TypeRef(bar) + ; foo = bar +Rule foo [rkType] + Union + TypeRef(uint) + TypeRef(tstr) + TypeRef(bstr) + ; foo = uint / tstr / bstr +Rule small [rkType] + Range(..) + Value(0) + Value(100) + ; small = 0..100 +Rule name [rkType] + Control(.size) + TypeRef(tstr) + Paren + Range(..) + Value(1) + Value(64) + ; name = tstr .size (1..64) +Rule my-type [rkType] + TypeRef(nil) + ; my-type = nil +Rule tagged [rkType] + Tagged(#6.1) + TypeRef(tstr) + ; tagged = #6.1(tstr) +Rule any-cbor [rkType] Any -Rule: foo [rkType] - SimpleType(bar) -Rule: g [rkType] - genericParams: @["T"] - Array[ - [ocZeroOrMore] - SimpleType(T) - ] -Rule: address [rkType] - Map{ - [ocOne] key=street(kkName) - SimpleType(tstr) - [ocOne] key=zip(kkName) - SimpleType(uint) - } -Rule: response [rkType] - Map{ - [ocOptional] key="err"(kkType) - SimpleType(tstr) - [ocOneOrMore] key="item"(kkType) - SimpleType(uint) - } -Rule: coord [rkType] - Array[ - [ocOne] - SimpleType(float) - [ocOne] - SimpleType(float) - ] -Rule: things [rkType] - Array[ - [ocZeroOrMore] - SimpleType(tstr) - ] -Rule: person [rkType] - Map{ - [ocOne] key=name(kkName) - SimpleType(tstr) - [ocOne] key=address(kkName) - Map{ - [ocOne] key=street(kkName) - SimpleType(tstr) - [ocOne] key=city(kkName) - SimpleType(tstr) - } - } -Rule: config [rkType] - Map{ - [ocOne] key=db(kkName) - Map{ - [ocOne] key=host(kkName) - SimpleType(tstr) - [ocOne] key=port(kkName) - SimpleType(uint) - } - [ocOptional] key=tls(kkName) - Map{ - [ocOne] key=cert(kkName) - SimpleType(bstr) - [ocOne] key=key(kkName) - SimpleType(bstr) - } - } -Rule: deep [rkType] - Map{ - [ocOne] key=a(kkName) - Map{ - [ocOne] key=b(kkName) - Map{ - [ocOne] key=c(kkName) - SimpleType(uint) - } - } - } -Rule: matrix [rkType] - Array[ - [ocOne] - Array[ - [ocOne] - SimpleType(float) - ] - ] -Rule: nested [rkType] - Array[ - [ocZeroOrMore] - Array[ - [ocOne] - SimpleType(uint) - [ocOne] - SimpleType(uint) - ] - ] -Rule: triple [rkType] - Array[ - [ocOne] - Array[ - [ocZeroOrMore] - SimpleType(tstr) - ] - [ocOne] - Array[ - [ocZeroOrMore] - SimpleType(uint) - ] - [ocOne] - Array[ - [ocZeroOrMore] - SimpleType(bstr) - ] - ] -Rule: table [rkType] - Array[ - [ocZeroOrMore] - Map{ - [ocOne] key=key(kkName) - SimpleType(tstr) - [ocOne] key=value(kkName) - SimpleType(uint) - } - ] -Rule: envelope [rkType] - Map{ - [ocOne] key=headers(kkName) - Array[ - [ocZeroOrMore] - SimpleType(tstr) - ] - [ocOne] key=payload(kkName) - Array[ - [ocOne] - SimpleType(uint) - [ocOne] - SimpleType(bstr) - ] - } -Rule: mixed [rkType] - Array[ - [ocOne] - Map{ - [ocOne] key=x(kkName) - SimpleType(float) - [ocOne] key=y(kkName) - SimpleType(float) - } - [ocZeroOrMore] - Map{ - [ocOne] key=label(kkName) - SimpleType(tstr) - } - ] -Rule: result [rkType] - Map{ - [ocOptional] key="ok"(kkType) - Map{ - [ocOne] key=code(kkName) - SimpleType(uint) - [ocOne] key=body(kkName) - SimpleType(tstr) - } - [ocOne] key="err"(kkType) - Map{ - [ocOne] key=msg(kkName) - SimpleType(tstr) - } - } -Rule: tree [rkType] - Array[ - [ocOne] - SimpleType(uint) - [ocZeroOrMore] - Array[ - [ocOne] - SimpleType(uint) - [ocOne] - Array[ - [ocZeroOrMore] - SimpleType(uint) - ] - ] - ] -Rule: profile [rkType] - Map{ - [ocOne] key=name(kkName) - SimpleType(tstr) - [ocOptional] key=email(kkName) - SimpleType(tstr) - [ocOptional] key=age(kkName) - SimpleType(uint) - } -Rule: settings [rkType] - Map{ - [ocOne] key=retries(kkName) - SimpleType(uint) - [ocOne] key=timeout-ms(kkName) - SimpleType(uint) - [ocOne] key=mode(kkName) - SimpleType(tstr) - } -Rule: status [rkType] - Union[ + ; any-cbor = # +Comment(; comment) +Rule foo [rkType] + TypeRef(bar) + ; foo = bar +Rule g [rkType] + Array + Entry [ocZeroOrMore] + TypeRef(T) + ; g = [* T] +Rule address [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(street) + TypeRef(tstr) + Entry [ocOne] key(skColon) + TypeRef(zip) + TypeRef(uint) + ; address = {street: tstr, zip: uint} +Rule response [rkType] + Map + Entry [ocOptional] key(skArrow) + Value("err") + TypeRef(tstr) + Entry [ocOneOrMore] key(skArrow) + Value("item") + TypeRef(uint) + ; response = {? "err" => tstr, + "item" => uint} +Rule coord [rkType] + Array + Entry [ocOne] + TypeRef(float) + Entry [ocOne] + TypeRef(float) + ; coord = [float, float] +Rule things [rkType] + Array + Entry [ocZeroOrMore] + TypeRef(tstr) + ; things = [* tstr] +Rule person [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(name) + TypeRef(tstr) + Entry [ocOne] key(skColon) + TypeRef(address) + Map + Entry [ocOne] key(skColon) + TypeRef(street) + TypeRef(tstr) + Entry [ocOne] key(skColon) + TypeRef(city) + TypeRef(tstr) + ; person = {name: tstr, address: {street: tstr, city: tstr}} +Rule config [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(db) + Map + Entry [ocOne] key(skColon) + TypeRef(host) + TypeRef(tstr) + Entry [ocOne] key(skColon) + TypeRef(port) + TypeRef(uint) + Entry [ocOptional] key(skColon) + TypeRef(tls) + Map + Entry [ocOne] key(skColon) + TypeRef(cert) + TypeRef(bstr) + Entry [ocOne] key(skColon) + TypeRef(key) + TypeRef(bstr) + ; config = {db: {host: tstr, port: uint}, ? tls: {cert: bstr, key: bstr}} +Rule deep [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(a) + Map + Entry [ocOne] key(skColon) + TypeRef(b) + Map + Entry [ocOne] key(skColon) + TypeRef(c) + TypeRef(uint) + ; deep = {a: {b: {c: uint}}} +Rule matrix [rkType] + Array + Entry [ocOne] + Array + Entry [ocOne] + TypeRef(float) + ; matrix = [[float]] +Rule nested [rkType] + Array + Entry [ocZeroOrMore] + Array + Entry [ocOne] + TypeRef(uint) + Entry [ocOne] + TypeRef(uint) + ; nested = [* [uint, uint]] +Rule triple [rkType] + Array + Entry [ocOne] + Array + Entry [ocZeroOrMore] + TypeRef(tstr) + Entry [ocOne] + Array + Entry [ocZeroOrMore] + TypeRef(uint) + Entry [ocOne] + Array + Entry [ocZeroOrMore] + TypeRef(bstr) + ; triple = [[* tstr], [* uint], [* bstr]] +Rule table [rkType] + Array + Entry [ocZeroOrMore] + Map + Entry [ocOne] key(skColon) + TypeRef(key) + TypeRef(tstr) + Entry [ocOne] key(skColon) + TypeRef(value) + TypeRef(uint) + ; table = [* {key: tstr, value: uint}] +Rule envelope [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(headers) + Array + Entry [ocZeroOrMore] + TypeRef(tstr) + Entry [ocOne] key(skColon) + TypeRef(payload) + Array + Entry [ocOne] + TypeRef(uint) + Entry [ocOne] + TypeRef(bstr) + ; envelope = {headers: [* tstr], payload: [uint, bstr]} +Rule mixed [rkType] + Array + Entry [ocOne] + Map + Entry [ocOne] key(skColon) + TypeRef(x) + TypeRef(float) + Entry [ocOne] key(skColon) + TypeRef(y) + TypeRef(float) + Entry [ocZeroOrMore] + Map + Entry [ocOne] key(skColon) + TypeRef(label) + TypeRef(tstr) + ; mixed = [{x: float, y: float}, * {label: tstr}] +Rule result [rkType] + Map + GroupChoice + Group + Entry [ocOptional] key(skArrow) + Value("ok") + Map + Entry [ocOne] key(skColon) + TypeRef(code) + TypeRef(uint) + Entry [ocOne] key(skColon) + TypeRef(body) + TypeRef(tstr) + Group + Entry [ocOne] key(skArrow) + Value("err") + Map + Entry [ocOne] key(skColon) + TypeRef(msg) + TypeRef(tstr) + ; result = {? "ok" => {code: uint, body: tstr} // "err" => {msg: tstr}} +Rule tree [rkType] + Array + Entry [ocOne] + TypeRef(uint) + Entry [ocZeroOrMore] + Array + Entry [ocOne] + TypeRef(uint) + Entry [ocOne] + Array + Entry [ocZeroOrMore] + TypeRef(uint) + ; tree = [uint, * [uint, [* uint]]] +Rule profile [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(name) + TypeRef(tstr) + Entry [ocOptional] key(skColon) + TypeRef(email) + TypeRef(tstr) + Entry [ocOptional] key(skColon) + TypeRef(age) + TypeRef(uint) + ; profile = {name: tstr, ? email: tstr, ? age: uint} +Rule settings [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(retries) + TypeRef(uint) + Entry [ocOne] key(skColon) + TypeRef(timeout-ms) + TypeRef(uint) + Entry [ocOne] key(skColon) + TypeRef(mode) + TypeRef(tstr) + ; settings = {retries: uint, timeout-ms: uint, mode: tstr} +Rule status [rkType] + Union Value("pending") Value("running") Value("done") Value("failed") - ] -Rule: port [rkType] - Union[ + ; status = "pending" / "running" / "done" / "failed" +Rule port [rkType] + Union Value(80) Value(443) Value(8080) - ] -Rule: bytes-id [rkType] - Generic(.size) -Rule: identifier [rkType] - Union[ - SimpleType(uint) - SimpleType(tstr) - SimpleType(bytes-id) - ] -Rule: short-name [rkType] - Generic(..<1,..<1,32>>) -Rule: sha256 [rkType] - Generic(.size) -Rule: resource-url [rkType] - Tagged(6.32) - SimpleType(tstr) -Rule: base64-data [rkType] - Tagged(6.21) - SimpleType(bstr) -Rule: box [rkType] - genericParams: @["T"] - Map{ - [ocOne] key=value(kkName) - SimpleType(T) - } -Rule: list [rkType] - genericParams: @["T"] - Array[ - [ocZeroOrMore] - SimpleType(T) - ] -Rule: dictionary [rkType] - genericParams: @["K", "V"] - Map{ - [ocZeroOrMore] key=K(kkType) - SimpleType(V) - } -Rule: response [rkType] - genericParams: @["T"] - Map{ - [ocOne] key=status(kkName) - SimpleType(uint) - [ocOne] key=payload(kkName) - SimpleType(T) - } -Rule: paged [rkType] - genericParams: @["T"] - Map{ - [ocOne] key=items(kkName) - Array[ - [ocZeroOrMore] - SimpleType(T) - ] - [ocOne] key=next-page(kkName) - SimpleType(uint) - } -Rule: bounded-list [rkType] - genericParams: @["T"] - Array[ - [ocRange] - SimpleType(T) - ] -Rule: success [rkType] - genericParams: @["T"] - Map{ - [ocOne] key=ok(kkName) - SimpleType(true) - [ocOne] key=value(kkName) - SimpleType(T) - } -Rule: failure [rkType] - Map{ - [ocOne] key=ok(kkName) - SimpleType(false) - [ocOne] key=error(kkName) - SimpleType(tstr) - } -Rule: result [rkType] - genericParams: @["T"] - Union[ - Generic(success) - SimpleType(failure) - ] -Rule: user [rkType] - Map{ - [ocOne] key=id(kkName) - SimpleType(uint) - [ocOne] key=name(kkName) - SimpleType(tstr) - } -Rule: encoded-user [rkType] - Generic(.cbor) -Rule: node [rkType] - Map{ - [ocOne] key=value(kkName) - SimpleType(int) - [ocOne] key=children(kkName) - Array[ - [ocZeroOrMore] - SimpleType(node) - ] - } -Rule: linked-node [rkType] - Map{ - [ocOne] key=value(kkName) - SimpleType(any) - [ocOptional] key=next(kkName) - SimpleType(linked-node) - } -Rule: opcode [rkType] - Group( - [ocOne] key=login + ; port = 80 / 443 / 8080 +Rule bytes-id [rkType] + Control(.size) + TypeRef(bstr) + Value(16) + ; bytes-id = bstr .size 16 +Rule identifier [rkType] + Union + TypeRef(uint) + TypeRef(tstr) + TypeRef(bytes-id) + ; identifier = uint / tstr / bytes-id +Rule short-name [rkType] + Control(.size) + TypeRef(tstr) + Paren + Range(..) + Value(1) + Value(32) + ; short-name = tstr .size (1..32) +Rule sha256 [rkType] + Control(.size) + TypeRef(bstr) + Value(32) + ; sha256 = bstr .size 32 +Rule resource-url [rkType] + Tagged(#6.32) + TypeRef(tstr) + ; resource-url = #6.32(tstr) +Rule base64-data [rkType] + Tagged(#6.21) + TypeRef(bstr) + ; base64-data = #6.21(bstr) +Rule box [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(value) + TypeRef(T) + ; box = {value: T} +Rule list [rkType] + Array + Entry [ocZeroOrMore] + TypeRef(T) + ; list = [* T] +Rule dictionary [rkType] + Map + Entry [ocZeroOrMore] key(skArrow) + TypeRef(K) + TypeRef(V) + ; dictionary = {* K => V} +Rule response [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(status) + TypeRef(uint) + Entry [ocOne] key(skColon) + TypeRef(payload) + TypeRef(T) + ; response = {status: uint, payload: T} +Rule paged [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(items) + Array + Entry [ocZeroOrMore] + TypeRef(T) + Entry [ocOne] key(skColon) + TypeRef(next-page) + TypeRef(uint) + ; paged = {items: [* T], next-page: uint} +Rule bounded-list [rkType] + Array + Entry [ocRange] 1*100 + TypeRef(T) + ; bounded-list = [1*100 T] +Rule pair [rkGroup] + Entry [ocOne] + Group + Entry [ocOne] key(skColon) + TypeRef(key) + TypeRef(K) + Entry [ocOne] key(skColon) + TypeRef(value) + TypeRef(V) + ; pair = (key: K, value: V) +Rule $$ext [rkGroupExt] + Entry [ocOne] + Group + Entry [ocOne] key(skColon) + TypeRef(payload) + TypeRef(T) + ; $$ext //= (payload: T) +Rule success [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(ok) + TypeRef(true) + Entry [ocOne] key(skColon) + TypeRef(value) + TypeRef(T) + ; success = {ok: true, value: T} +Rule failure [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(ok) + TypeRef(false) + Entry [ocOne] key(skColon) + TypeRef(error) + TypeRef(tstr) + ; failure = {ok: false, error: tstr} +Rule result [rkType] + Union + Generic(success) + TypeRef(T) + TypeRef(failure) + ; result = success / failure +Rule user [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(id) + TypeRef(uint) + Entry [ocOne] key(skColon) + TypeRef(name) + TypeRef(tstr) + ; user = {id: uint, name: tstr} +Rule encoded-user [rkType] + Control(.cbor) + TypeRef(bstr) + TypeRef(user) + ; encoded-user = bstr .cbor user +Rule node [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(value) + TypeRef(int) + Entry [ocOne] key(skColon) + TypeRef(children) + Array + Entry [ocZeroOrMore] + TypeRef(node) + ; node = {value: int, children: [* node]} +Rule linked-node [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(value) + TypeRef(any) + Entry [ocOptional] key(skColon) + TypeRef(next) + TypeRef(linked-node) + ; linked-node = {value: any, ? next: linked-node} +Rule opcode [rkType] + Enum + Group + Entry [ocOne] key(skColon) + TypeRef(login) + Value(1) + Entry [ocOne] key(skColon) + TypeRef(logout) + Value(2) + Entry [ocOne] key(skColon) + TypeRef(ping) + Value(3) + Entry [ocOne] key(skColon) + TypeRef(pong) + Value(4) + ; opcode = &(login: 1, logout: 2, ping: 3, pong: 4) +Rule common-fields [rkGroup] + Entry [ocOne] + Group + Entry [ocOne] key(skColon) + TypeRef(id) + TypeRef(uint) + Entry [ocOne] key(skColon) + TypeRef(created-at) + TypeRef(uint) + ; common-fields = (id: uint, created-at: uint) +Rule article [rkType] + Map + Entry [ocOne] + TypeRef(common-fields) + Entry [ocOne] key(skColon) + TypeRef(title) + TypeRef(tstr) + Entry [ocOne] key(skColon) + TypeRef(body) + TypeRef(tstr) + ; article = {common-fields, title: tstr, body: tstr} +Rule open-metadata [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(version) + TypeRef(uint) + Entry [ocZeroOrMore] key(skArrow) + TypeRef(tstr) + TypeRef(any) + ; open-metadata = {version: uint, * tstr => any} +Rule sensor-values [rkType] + Map + Entry [ocOne] key(skArrow) Value(1) - [ocOne] key=logout + TypeRef(float) + Entry [ocOne] key(skArrow) Value(2) - [ocOne] key=ping + TypeRef(float) + Entry [ocOne] key(skArrow) Value(3) - [ocOne] key=pong - Value(4) - ) -Rule: common-fields [rkGroup] - Group( - [ocOne] key=id - SimpleType(uint) - [ocOne] key=created-at - SimpleType(uint) - ) -Rule: article [rkType] - Map{ - [ocOne] - SimpleType(common-fields) - [ocOne] key=title(kkName) - SimpleType(tstr) - [ocOne] key=body(kkName) - SimpleType(tstr) - } -Rule: open-metadata [rkType] - Map{ - [ocOne] key=version(kkName) - SimpleType(uint) - [ocZeroOrMore] key=tstr(kkType) - SimpleType(any) - } -Rule: sensor-values [rkType] - Map{ - [ocOne] key=1(kkType) - SimpleType(float) - [ocOne] key=2(kkType) - SimpleType(float) - [ocOne] key=3(kkType) - SimpleType(float) - } -Rule: user [rkType] - Map{ - [ocOne] key=id(kkName) - SimpleType(uint) - [ocOne] key=name(kkName) - SimpleType(tstr) - } -Rule: page [rkType] - genericParams: @["T"] - Map{ - [ocOne] key=items(kkName) - Array[ - [ocZeroOrMore] - SimpleType(T) - ] - [ocOne] key=total(kkName) - SimpleType(uint) - [ocOne] key=page(kkName) - SimpleType(uint) - [ocOne] key=page-size(kkName) - SimpleType(uint) - } -Rule: api-envelope [rkType] - genericParams: @["T"] - Map{ - [ocOne] key=trace-id(kkName) - SimpleType(tstr) - [ocOne] key=timestamp(kkName) - SimpleType(uint) - [ocOne] key=payload(kkName) - SimpleType(T) - } -Rule: user-page-envelope [rkType] - Generic(api-envelope>) -Rule: api-response [rkType] - genericParams: @["T"] - Map{ - [ocOne] key=code(kkName) - SimpleType(uint) - [ocOne] key=success(kkName) - SimpleType(bool) - [ocOptional] key=data(kkName) - SimpleType(T) - [ocOptional] key=error(kkName) - Map{ - [ocOne] key=message(kkName) - SimpleType(tstr) - [ocOne] key=details(kkName) - SimpleType(tstr) - } - } -Rule: message-type [rkType] - Union[ + TypeRef(float) + ; sensor-values = {1 => float, 2 => float, 3 => float} +Rule user [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(id) + TypeRef(uint) + Entry [ocOne] key(skColon) + TypeRef(name) + TypeRef(tstr) + ; user = {id: uint, name: tstr} +Rule page [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(items) + Array + Entry [ocZeroOrMore] + TypeRef(T) + Entry [ocOne] key(skColon) + TypeRef(total) + TypeRef(uint) + Entry [ocOne] key(skColon) + TypeRef(page) + TypeRef(uint) + Entry [ocOne] key(skColon) + TypeRef(page-size) + TypeRef(uint) + ; page = {items: [* T], total: uint, page: uint, page-size: uint} +Rule api-envelope [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(trace-id) + TypeRef(tstr) + Entry [ocOne] key(skColon) + TypeRef(timestamp) + TypeRef(uint) + Entry [ocOne] key(skColon) + TypeRef(payload) + TypeRef(T) + ; api-envelope = {trace-id: tstr, timestamp: uint, payload: T} +Rule user-page-envelope [rkType] + Generic(api-envelope) + Generic(page) + TypeRef(user) + ; user-page-envelope = api-envelope> +Rule api-response [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(code) + TypeRef(uint) + Entry [ocOne] key(skColon) + TypeRef(success) + TypeRef(bool) + Entry [ocOptional] key(skColon) + TypeRef(data) + TypeRef(T) + Entry [ocOptional] key(skColon) + TypeRef(error) + Map + Entry [ocOne] key(skColon) + TypeRef(message) + TypeRef(tstr) + Entry [ocOne] key(skColon) + TypeRef(details) + TypeRef(tstr) + ; api-response = {code: uint, success: bool, ? data: T, ? error: {message: tstr, details: tstr}} +Rule message-type [rkType] + Union Value(1) Value(2) Value(3) - ] -Rule: header [rkType] - Map{ - [ocOne] key=version(kkName) + ; message-type = 1 / 2 / 3 +Rule header [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(version) Value(1) - [ocOne] key=msg-type(kkName) - SimpleType(message-type) - [ocOne] key=request-id(kkName) - SimpleType(uint) - } -Rule: login-payload [rkType] - Map{ - [ocOne] key=username(kkName) - SimpleType(tstr) - [ocOne] key=password(kkName) - SimpleType(tstr) - } -Rule: ping-payload [rkType] - Map{ - [ocOne] key=timestamp(kkName) - SimpleType(uint) - } -Rule: payload [rkType] - Union[ - SimpleType(login-payload) - SimpleType(ping-payload) - ] -Rule: packet [rkType] - Map{ - [ocOne] key=header(kkName) - SimpleType(header) - [ocOne] key=body(kkName) - SimpleType(payload) - } -Rule: plugin-config [rkType] - Map{ - [ocOne] key=name(kkName) - SimpleType(tstr) - [ocOne] key=config(kkName) - SimpleType(any) - } + Entry [ocOne] key(skColon) + TypeRef(msg-type) + TypeRef(message-type) + Entry [ocOne] key(skColon) + TypeRef(request-id) + TypeRef(uint) + ; header = {version: 1, msg-type: message-type, request-id: uint} +Rule login-payload [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(username) + TypeRef(tstr) + Entry [ocOne] key(skColon) + TypeRef(password) + TypeRef(tstr) + ; login-payload = {username: tstr, password: tstr} +Rule ping-payload [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(timestamp) + TypeRef(uint) + ; ping-payload = {timestamp: uint} +Rule payload [rkType] + Union + TypeRef(login-payload) + TypeRef(ping-payload) + ; payload = login-payload / ping-payload +Rule packet [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(header) + TypeRef(header) + Entry [ocOne] key(skColon) + TypeRef(body) + TypeRef(payload) + ; packet = {header: header, body: payload} +Rule plugin-config [rkType] + Map + Entry [ocOne] key(skColon) + TypeRef(name) + TypeRef(tstr) + Entry [ocOne] key(skColon) + TypeRef(config) + TypeRef(any) + ; plugin-config = {name: tstr, config: any} +Rule my-first-rule [rkType] + TypeRef(int) + ; my-first-rule = int +Rule my-rule [rkType] + Union + TypeRef(int) + TypeRef(tstr) + ; my-rule = int / tstr +Rule my-rule [rkType] + TypeRef(int) + ; my-rule = int +Rule my-rule [rkTypeExt] + TypeRef(tstr) + ; my-rule /= tstr +Rule message-type [rkType] + Value(1) + ; message-type = 1 +Rule protocol-version [rkType] + Value("1.0") + ; protocol-version = "1.0" +Rule fixed-header [rkType] + Value(h'cafef00d') + ; fixed-header = h'cafef00d' +Rule status-code [rkType] + Union + Value(200) + Value(404) + Value(500) + ; status-code = 200 / 404 / 500 +Rule triplet [rkType] + Array + Entry [ocOne] + TypeRef(uint) + Entry [ocOne] + TypeRef(uint) + Entry [ocOne] + TypeRef(uint) + ; triplet = [uint, uint, uint] +Rule empty-array [rkType] + Array + ; empty-array = [] +Rule mixed-array [rkType] + Array + Entry [ocOne] + TypeRef(bool) + Entry [ocOne] + Union + TypeRef(int) + TypeRef(null) + ; mixed-array = [bool, int / null] +Rule simple-object [rkType] + Map + Entry [ocOne] key(skColon) + Value("name") + TypeRef(tstr) + Entry [ocOne] key(skColon) + Value("age") + TypeRef(uint) + Entry [ocOne] key(skColon) + TypeRef(is-verified) + TypeRef(bool) + ; simple-object = {"name": tstr, "age": uint, is-verified: bool} +Rule indexed-data [rkType] + Map + Entry [ocOne] key(skArrow) + Value(1) + TypeRef(tstr) + Entry [ocOne] key(skArrow) + Value(2) + TypeRef(bstr) + Entry [ocOptional] key(skArrow) + Value(3) + TypeRef(float) + ; indexed-data = {1 => tstr, 2 => bstr, ? 3 => float} +Rule lookup-table [rkType] + Map + Entry [ocZeroOrMore] key(skArrow) + TypeRef(uint) + TypeRef(tstr) + ; lookup-table = {* uint => tstr} +Rule empty-map [rkType] + Map + ; empty-map = {} +Rule record-header [rkGroup] + Entry [ocOne] + Group + Entry [ocOne] + TypeRef(uint) + Entry [ocOne] + TypeRef(tstr) + ; record-header = (uint, tstr) +Rule point-2d [rkGroup] + Entry [ocOne] + Group + Entry [ocOne] + TypeRef(float) + Entry [ocOne] + TypeRef(float) + ; point-2d = (float, float) +Rule address [rkGroup] + Entry [ocOne] + Group + Entry [ocOne] key(skColon) + TypeRef(street) + TypeRef(tstr) + Entry [ocOne] key(skColon) + TypeRef(city) + TypeRef(tstr) + Entry [ocOne] key(skColon) + TypeRef(zip) + TypeRef(uint) + ; address = (street: tstr, city: tstr, zip: uint) +Rule optional-id [rkType] + Array + Entry [ocOptional] + TypeRef(uint) + ; optional-id = [? uint] +Rule config [rkGroup] + Entry [ocOne] + Group + Entry [ocOne] + TypeRef(tstr) + Entry [ocOptional] + TypeRef(bool) + ; config = (tstr, ? bool) +Rule int-list [rkType] + Array + Entry [ocZeroOrMore] + TypeRef(int) + ; int-list = [* int] +Rule byte-chunks [rkGroup] + Entry [ocOne] + Group + Entry [ocZeroOrMore] + TypeRef(bstr) + ; byte-chunks = (* bstr) +Rule non-empty-list [rkType] + Array + Entry [ocOneOrMore] + TypeRef(tstr) + ; non-empty-list = [+ tstr] +Rule data-record [rkGroup] + Entry [ocOne] + Group + Entry [ocOne] + TypeRef(uint) + Entry [ocOneOrMore] + TypeRef(float) + ; data-record = (uint, + float) +Rule rgb-color [rkType] + Array + Entry [ocRange] 3*3 + TypeRef(uint) + ; rgb-color = [3*3 uint] +Rule short-ids [rkType] + Array + Entry [ocRange] 1*5 + TypeRef(int) + ; short-ids = [1*5 int] +Rule max-10-items [rkType] + Array + Entry [ocRange] *10 + TypeRef(any) + ; max-10-items = [*10 any] +Rule at-least-2 [rkType] + Array + Entry [ocRange] 2* + TypeRef(bstr) + ; at-least-2 = [2* bstr] +Rule identifier [rkType] + Union + TypeRef(tstr) + TypeRef(uint) + ; identifier = tstr / uint +Rule config-value [rkType] + Union + TypeRef(bool) + TypeRef(int) + TypeRef(tstr) + TypeRef(null) + ; config-value = bool / int / tstr / null +Rule measurement [rkType] + Array + Entry [ocOne] + TypeRef(tstr) + Entry [ocOne] + Union + TypeRef(int) + TypeRef(float) + ; measurement = [tstr, int / float] +Rule contact-method [rkType] + Map + GroupChoice + Group + Entry [ocOne] + Group + Entry [ocOne] key(skColon) + TypeRef(email) + TypeRef(tstr) + Group + Entry [ocOne] + Group + Entry [ocOne] key(skColon) + TypeRef(phone) + TypeRef(tstr) + Group + Entry [ocOne] + TypeRef(postal-address) + ; contact-method = {(email: tstr) // (phone: tstr) // postal-address} +Rule response [rkType] + Map + GroupChoice + Group + Entry [ocOne] + Group + Entry [ocOne] key(skColon) + TypeRef(status) + Value(200) + Entry [ocOne] key(skColon) + TypeRef(body) + TypeRef(bstr) + Group + Entry [ocOne] + Group + Entry [ocOne] key(skColon) + TypeRef(status) + Value(500) + Entry [ocOne] key(skColon) + TypeRef(error) + TypeRef(tstr) + ; response = {(status: 200, body: bstr) // (status: 500, error: tstr)} +Rule age [rkType] + Control(.le) + TypeRef(uint) + Value(120) + ; age = uint .le 120 +Rule percentage [rkType] + Range(..) + Value(0) + Value(100) + ; percentage = 0..100 +Rule temperature [rkType] + Range(..) + Value(-40) + Value(50) + ; temperature = -40..50 +Rule http-status-ok [rkType] + Range(..) + Value(200) + Value(299) + ; http-status-ok = 200..299 +Rule first-byte [rkType] + Range(..) + Value(0x00) + Value(0xFF) + ; first-byte = 0x00..0xFF +Rule short-string [rkType] + Control(.size) + TypeRef(tstr) + Paren + Range(..) + Value(1) + Value(64) + ; short-string = tstr .size (1..64) +Rule sha256-hash [rkType] + Control(.size) + TypeRef(bstr) + Value(32) + ; sha256-hash = bstr .size 32 +Rule coordinate [rkType] + Control(.size) + Array + Entry [ocOne] + TypeRef(float) + Value(2) + ; coordinate = [float] .size 2 +Rule simple-map [rkType] + Control(.size) + Map + Entry [ocZeroOrMore] key(skArrow) + TypeRef(tstr) + TypeRef(any) + Paren + Range(..) + Value(1) + Value(5) + ; simple-map = {* tstr => any} .size (1..5) +Rule email [rkType] + Control(.regexp) + TypeRef(tstr) + Value("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}") + ; email = tstr .regexp "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}" +Rule iso-date [rkType] + Control(.regexp) + TypeRef(tstr) + Value("\d{4}-\d{2}-\d{2}") + ; iso-date = tstr .regexp "\d{4}-\d{2}-\d{2}" +Rule payload [rkType] + Control(.cbor) + TypeRef(bstr) + TypeRef(any) + ; payload = bstr .cbor any +Rule message-stream [rkType] + Control(.cborseq) + TypeRef(bstr) + TypeRef(log-entry) + ; message-stream = bstr .cborseq log-entry diff --git a/tests/test_cddl_parser_dump_pretty.txt b/tests/test_cddl_parser_dump_pretty.txt new file mode 100644 index 0000000..17c2475 --- /dev/null +++ b/tests/test_cddl_parser_dump_pretty.txt @@ -0,0 +1,685 @@ +person = { + age: int, + name: tstr, + employer: tstr, +} +pii = ( + age: int, + name: tstr, + employer: tstr, +) +person = {pii} +person = {( + age: int, + name: tstr, + employer: tstr, +)} +person = { + age: int, + name: tstr, + employer: tstr, +} + +dog = { + age: int, + name: tstr, + leash-length: float, +} +person = { + identity, + employer: tstr, +} + +dog = { + identity, + leash-length: float, +} + +identity = ( + age: int, + name: tstr, +) +attire = "bow tie" / "necktie" / "Internet attire" +protocol = 6 / 17 +address = {delivery} + +delivery = ( + street: tstr, ? number: uint, city // + po-box: uint, city // + per-pickup: true +) + +city = ( + name: tstr, + zip-code: uint, +) +attire /= "swimwear" + +delivery //= ( + lat: float, + long: float, + drone-type: tstr, +) +device-address = byte +max-byte = 255 +byte = 0..max-byte ; inclusive range +first-non-byte = 256 +byte1 = 0...first-non-byte ; byte1 is equivalent to byte +int-range = 0..10 ; only integers match +float-range = 0.0..10.0 ; only floats match +BAD-range1 = 0..10.0 ; NOT DEFINED +BAD-range2 = 0.0..10 ; NOT DEFINED +numeric-range = int-range / float-range +terminal-color = &basecolors +basecolors = ( + black: 0, + red: 1, + green: 2, + yellow: 3, + blue: 4, + magenta: 5, + cyan: 6, + white: 7, +) +extended-color = &( + basecolors, + orange: 8, + pink: 9, + purple: 10, + brown: 11, +) +my_breakfast = #6.55799(breakfast) ; cbor-any is too general! +breakfast = cereal / porridge +cereal = #6.998(tstr) +porridge = #6.999([ + liquid, + solid, +]) +liquid = milk / water +milk = 0 +water = 1 +solid = tstr +; This is a comment +person = {g} + +g = ( + "name": tstr, + age: int, ; "age" is a bareword +) +apartment = { + kitchen: size, + * bedroom: size, +} +size = float ; in m2 +unlimited-people = [* person] +one-or-two-people = [1*2 person] +at-least-two-people = [2* person] +person = ( + name: tstr, + age: uint, +) +Geography = [ + city: tstr, + gpsCoordinates: GpsCoordinates, +] + +GpsCoordinates = { + longitude: uint, ; degrees, scaled by 10^7 + latitude: uint, ; degrees, scaled by 10^7 +} +located-samples = { + sample-point: int, + samples: [+ float], +} +located-samples = { + "sample-point" => int, + "samples" => [+ float], +} +located-samples = { + sample-point: int, + samples: [+ float], + * equipment-type => equipment-tolerances, +} +equipment-type = [ + name: tstr, + manufacturer: tstr, +] +equipment-tolerances = [+ [ + float, + float, +]] +PersonalData = { + ? displayName: tstr, + NameComponents, + ? age: uint, +} + +NameComponents = ( + ? firstName: tstr, + ? familyName: tstr, +) +PersonalData = { + ? displayName: tstr, + NameComponents, + ? age: uint, + * tstr => any, +} + +NameComponents = ( + ? firstName: tstr, + ? familyName: tstr, +) +square-roots = { + * x => y, +} +x = int +y = float +tostring = { + * mynumber => tstr, +} +mynumber = int / float +labeled-values = { + ? fritz: number, + * label => value, +} +label = text +value = number +do-not-do-this = { + int => int, + int => 6, +} +extensible-map-example = { + ? "optional-key" => int, + * tstr => any, +} +extensible-map-example = { + ? "optional-key" ^ => int, + * tstr => any, +} +extensible-map-example = { + ? "optional-key": int, + * tstr => any, +} +extensible-map-example = { + ? optional-key: int, + * tstr => any, +} +buuid = #6.37(bstr) +my_uri = #6.32(tstr) / tstr +basic-header-group = ( + field1: int, + field2: text, +) + +basic-header = [basic-header-group] + +advanced-header = [ + basic-header-group, + field3: bytes, + field4: number, ; as in the tagged type "time" +] +basic-header = [ + field1: int, + field2: text, +] + +advanced-header = [ + ~basic-header, + field3: bytes, + field4: ~time, +] +full-address = [ + [+ label], + ip4, + ip6, +] +ip4 = bstr .size 4 +ip6 = bstr .size 16 +label = bstr .size (1..63) +audio_sample = uint .size 3 ; 24-bit, equivalent to 0...16777216 +tcpflagbytes = bstr .bits flags +flags = &( + fin: 8, + syn: 9, + rst: 10, + psh: 11, + ack: 12, + urg: 13, + ece: 14, + cwr: 15, + ns: 0, + ) / + (4..7) ; data offset bits + +rwxbits = uint .bits rwx +rwx = &( + r: 2, + w: 1, + x: 0, +) +nai = tstr .regexp "[A-Za-z0-9]+@[A-Za-z0-9]+(\\.[A-Za-z0-9]+)+" +message = $message .within message-structure +message-structure = [ + message_type, + * message_option, +] +message_type = 0..255 +message_option = any + +$message /= [ + 3, + dough: text, + topping: [* text], +] +$message /= [ + 4, + noodles: text, + sauce: text, + parmesan: bool, +] +speed = number .ge 0 ; unit: m/s +timer = { + time: uint, + ? displayed-step: (number .gt 0) .default 1, +} +tcp-header = { + seq: uint, + ack: uint, + * $$tcp-option, +} + +; later, in a different file + +$$tcp-option //= ( + sack: [+ ( + left: uint, + right: uint, + )], +) + +; and, maybe in another file + +$$tcp-option //= ( + sack-permitted: true, +) +PersonalData = { + ? displayName: tstr, + NameComponents, + ? age: uint, + * $$personaldata-extensions, +} + +NameComponents = ( + ? firstName: tstr, + ? familyName: tstr, +) + +; The above already works as is. +; But then, we can add later: + +$$personaldata-extensions //= ( + favorite-salsa: tstr, +) + +; and again, somewhere else: + +$$personaldata-extensions //= ( + shoesize: uint, +) +messages = message<"reboot", "now"> / message<"sleep", 1..100> +message = { + type: t, + value: v, +} +t = [group1] +group1 = ( + a / b // + c / d +) +a = 1 +b = 2 +c = 3 +d = 4 +t = {group2} +group2 = ( + ? ab: a / b // + cd: c / d +) +a = 1 +b = 2 +c = 3 +d = 4 +t = [group3] +group3 = (+ a / b / c) +a = 1 +b = 2 +c = 3 +t = [group4] +group4 = ( + + a // + b / c +) +a = 1 +b = 2 +c = 3 +t = [group4a] +group4a = ( + (+ a) // + (b / c) +) +a = 1 +b = 2 +c = 3 +foo = bar +foo = uint / tstr / bstr +small = 0..100 +name = tstr .size (1..64) +my-type = nil +tagged = #6.1(tstr) +any-cbor = # +; comment +foo = bar +g = [* T] +address = { + street: tstr, + zip: uint, +} +response = { + ? "err" => tstr, + + "item" => uint, +} +coord = [ + float, + float, +] +things = [* tstr] +person = { + name: tstr, + address: { + street: tstr, + city: tstr, + }, +} +config = { + db: { + host: tstr, + port: uint, + }, + ? tls: { + cert: bstr, + key: bstr, + }, +} +deep = { + a: { + b: { + c: uint, + }, + }, +} +matrix = [[float]] +nested = [* [ + uint, + uint, +]] +triple = [ + [* tstr], + [* uint], + [* bstr], +] +table = [* { + key: tstr, + value: uint, +}] +envelope = { + headers: [* tstr], + payload: [ + uint, + bstr, + ], +} +mixed = [ + { + x: float, + y: float, + }, + * { + label: tstr, + }, +] +result = { + ? "ok" => {code: uint, body: tstr} // + "err" => {msg: tstr} +} +tree = [ + uint, + * [ + uint, + [* uint], + ], +] +profile = { + name: tstr, + ? email: tstr, + ? age: uint, +} +settings = { + retries: uint, + timeout-ms: uint, + mode: tstr, +} +status = "pending" / "running" / "done" / "failed" +port = 80 / 443 / 8080 +bytes-id = bstr .size 16 +identifier = uint / tstr / bytes-id +short-name = tstr .size (1..32) +sha256 = bstr .size 32 +resource-url = #6.32(tstr) +base64-data = #6.21(bstr) +box = { + value: T, +} +list = [* T] +dictionary = { + * K => V, +} +response = { + status: uint, + payload: T, +} +paged = { + items: [* T], + next-page: uint, +} +bounded-list = [1*100 T] +pair = ( + key: K, + value: V, +) +$$ext //= ( + payload: T, +) +success = { + ok: true, + value: T, +} +failure = { + ok: false, + error: tstr, +} +result = success / failure +user = { + id: uint, + name: tstr, +} +encoded-user = bstr .cbor user +node = { + value: int, + children: [* node], +} +linked-node = { + value: any, + ? next: linked-node, +} +opcode = &( + login: 1, + logout: 2, + ping: 3, + pong: 4, +) +common-fields = ( + id: uint, + created-at: uint, +) +article = { + common-fields, + title: tstr, + body: tstr, +} +open-metadata = { + version: uint, + * tstr => any, +} +sensor-values = { + 1 => float, + 2 => float, + 3 => float, +} +user = { + id: uint, + name: tstr, +} +page = { + items: [* T], + total: uint, + page: uint, + page-size: uint, +} +api-envelope = { + trace-id: tstr, + timestamp: uint, + payload: T, +} +user-page-envelope = api-envelope> +api-response = { + code: uint, + success: bool, + ? data: T, + ? error: { + message: tstr, + details: tstr, + }, +} +message-type = 1 / 2 / 3 +header = { + version: 1, + msg-type: message-type, + request-id: uint, +} +login-payload = { + username: tstr, + password: tstr, +} +ping-payload = { + timestamp: uint, +} +payload = login-payload / ping-payload +packet = { + header: header, + body: payload, +} +plugin-config = { + name: tstr, + config: any, +} +my-first-rule = int +my-rule = int / tstr +my-rule = int +my-rule /= tstr +message-type = 1 +protocol-version = "1.0" +fixed-header = h'cafef00d' +status-code = 200 / 404 / 500 +triplet = [ + uint, + uint, + uint, +] +empty-array = [] +mixed-array = [ + bool, + int / null, +] +simple-object = { + "name": tstr, + "age": uint, + is-verified: bool, +} +indexed-data = { + 1 => tstr, + 2 => bstr, + ? 3 => float, +} +lookup-table = { + * uint => tstr, +} +empty-map = {} +record-header = ( + uint, + tstr, +) +point-2d = ( + float, + float, +) +address = ( + street: tstr, + city: tstr, + zip: uint, +) +optional-id = [? uint] +config = ( + tstr, + ? bool, +) +int-list = [* int] +byte-chunks = (* bstr) +non-empty-list = [+ tstr] +data-record = ( + uint, + + float, +) +rgb-color = [3*3 uint] +short-ids = [1*5 int] +max-10-items = [*10 any] +at-least-2 = [2* bstr] +identifier = tstr / uint +config-value = bool / int / tstr / null +measurement = [ + tstr, + int / float, +] +contact-method = { + (email: tstr) // + (phone: tstr) // + postal-address +} +response = { + (status: 200, body: bstr) // + (status: 500, error: tstr) +} +age = uint .le 120 +percentage = 0..100 +temperature = -40..50 +http-status-ok = 200..299 +first-byte = 0x00..0xFF +short-string = tstr .size (1..64) +sha256-hash = bstr .size 32 +coordinate = [float] .size 2 +simple-map = { + * tstr => any, +} .size (1..5) +email = tstr .regexp "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}" +iso-date = tstr .regexp "\d{4}-\d{2}-\d{2}" +payload = bstr .cbor any +message-stream = bstr .cborseq log-entry diff --git a/tests/test_cddl_parser_old.nim b/tests/test_cddl_parser_old.nim new file mode 100644 index 0000000..b93ed71 --- /dev/null +++ b/tests/test_cddl_parser_old.nim @@ -0,0 +1,29 @@ +# cbor-serialization +# Copyright (c) 2025 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +import unittest2, ../cbor_serialization/cddl/parser + +const testSpecCases = [ + """ +person = { + age: int, + name: tstr, + employer: tstr, +} + """ +] + +suite "Test CDDL parser (deprecated)": + dualTest "parse valid test cases": + for t in testSpecCases: + try: + discard parseCddl(t) + except CborCddlError: + checkpoint("FAILED: " & t) + fail() diff --git a/tests/test_writer.nim b/tests/test_writer.nim index c148898..a83f090 100644 --- a/tests/test_writer.nim +++ b/tests/test_writer.nim @@ -199,6 +199,8 @@ suite "Test writer": check w.hex == "0x6132" checkCbor w, "2" + Cbor.enumRep(DefaultFlavor, EnumAsString) + test "EnumAsString of DefaultFlavor/Cbor": type Fruit = enum @@ -259,6 +261,8 @@ suite "Test writer": check v.hex == "0x634f6e65" checkCbor v, "One" + Cbor.flavorEnumRep(EnumAsString) + type DefinText = distinct string IndefText = distinct seq[string]