wmtools/nimbledeps/pkgs2/parsetoml-0.7.1-586fe63467a674008c4445ed1b8ac882177d7103/parsetoml.nim

1943 lines
67 KiB
Nim
Raw Normal View History

2023-11-23 21:10:04 +01:00
## :License: MIT
##
## Introduction
## ============
## This module implements a TOML parser that is compliant with v0.5.0 of its spec.
##
## Source
## ======
## `Repo link <https://github.com/NimParsers/parsetoml>`_
##
# Copyright (c) 2015 Maurizio Tomasi and contributors
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import math
import streams
import strutils
import tables
import unicode
from parseutils import parseFloat
export tables
when (NimMajor, NimMinor, NimPatch) < (1, 4, 0):
type
IndexDefect* = IndexError
OverflowDefect* = OverflowError
type
Sign* = enum None, Pos, Neg
TomlValueKind* {.pure.} = enum
None
Int,
Float,
Bool,
Datetime,
Date,
Time,
String,
Array,
Table
TomlDate* = object
year*: int
month*: int
day*: int
TomlTime* = object
hour*: int
minute*: int
second*: int
subsecond*: int
TomlDateTime* = object
date*: TomlDate
time*: TomlTime
case shift*: bool
of true:
isShiftPositive*: bool
zoneHourShift*: int
zoneMinuteShift*: int
of false: nil
TomlTable* = OrderedTable[string, TomlValueRef]
TomlTableRef* = ref TomlTable
TomlValueRef* = ref TomlValue
TomlValue* = object
case kind*: TomlValueKind
of TomlValueKind.None: nil
of TomlValueKind.Int: intVal*: int64
of TomlValueKind.Float:
floatVal*: float64
forcedSign*: Sign
of TomlValueKind.Bool: boolVal*: bool
of TomlValueKind.Datetime: dateTimeVal*: TomlDateTime
of TomlValueKind.Date: dateVal*: TomlDate
of TomlValueKind.Time: timeVal*: TomlTime
of TomlValueKind.String: stringVal*: string
of TomlValueKind.Array: arrayVal*: seq[TomlValueRef]
of TomlValueKind.Table: tableVal*: TomlTableRef
ParserState = object
fileName*: string
line*: int
column*: int
pushback: char
stream*: streams.Stream
curTableRef*: TomlTableRef
TomlError* = object of ValueError
location*: ParserState
NumberBase = enum
base10, base16, base8, base2
StringType {.pure.} = enum
Basic, # Enclosed within double quotation marks
Literal # Enclosed within single quotation marks
const
defaultStringCapacity = 256
ctrlChars = {'\x00' .. '\x08', '\x0A' .. '\x1F', '\x7F'} # '\x09' - TAB is not counted as control char
ctrlCharsExclCrLf = ctrlChars - {'\x0A', '\x0D'}
proc newTomlError(location: ParserState, msg: string): ref TomlError =
result = newException(TomlError, location.fileName & "(" & $location.line &
":" & $location.column & ")" & " " & msg)
result.location = location
proc getNextChar(state: var ParserState): char =
# Return the next available char from the stream associate with
# the parser state, or '\0' if there are no characters left.
if state.pushback != '\0':
# If we've just read a character without having interpreted
# it, just return it
result = state.pushback
state.pushback = '\0'
else:
if state.stream.atEnd():
return '\0'
result = state.stream.readChar()
# Update the line and column number
if result == '\l':
inc(state.line)
state.column = 1
elif result != '\r':
inc(state.column)
proc pushBackChar(state: var ParserState, c: char) {.inline.} =
state.pushback = c
type
LfSkipMode = enum
skipLf, skipNoLf
proc getNextNonWhitespace(state: var ParserState,
skip: LfSkipMode): char =
# Note: this procedure does *not* consider a newline as a
# "whitespace". Since newlines are often mandatory in TOML files
# (e.g. after a key/value specification), we do not want to miss
# them...
let whitespaces = (case skip
of skipLf: {' ', '\t', '\r', '\l'}
of skipNoLf: {' ', '\t', '\r'})
var nextChar: char
while true:
nextChar = state.getNextChar()
if nextChar == '#':
# Skip the comment up to the newline, but do not jump over it
while nextChar != '\l' and nextChar != '\0':
nextChar = state.getNextChar()
# https://toml.io/en/v1.0.0#comment
# Control characters other than tab (U+0009) are not permitted in comments.
# Invalid control characters: U+0000 to U+0008, U+000A to U+001F, U+007F
if nextChar in ctrlCharsExclCrLf:
raise newTomlError(state, "invalid control char 0x$# found in a comment" % [nextChar.ord.toHex(2)])
if nextChar notin whitespaces: break
result = nextChar
proc charToInt(c: char, base: NumberBase): int {.inline, noSideEffect.} =
case base
of base10, base8, base2: result = int(c) - int('0')
of base16:
if c in strutils.Digits:
result = charToInt(c, base10)
else:
result = 10 + int(toUpperAscii(c)) - int('A')
type
LeadingChar {.pure.} = enum
AllowZero, DenyZero
proc parseInt(state: var ParserState,
base: NumberBase,
leadingChar: LeadingChar): int64 =
var
nextChar: char
firstPos = true
negative = false
wasUnderscore = false
let
baseNum = (case base
of base2: 2
of base8: 8
of base10: 10
of base16: 16)
digits = (case base
of base2: {'0', '1'}
of base8: {'0', '1', '2', '3', '4', '5', '6', '7'}
of base10: strutils.Digits
of base16: strutils.HexDigits)
result = 0
while true:
wasUnderscore = nextChar == '_'
nextChar = state.getNextChar()
if nextChar == '_':
if firstPos or wasUnderscore:
raise(newTomlError(state,
"underscore must be surrounded by digit"))
continue
if nextChar in {'+', '-'} and firstPos:
firstPos = false
if nextChar == '-': negative = true
continue
if nextChar == '0' and firstPos and leadingChar == LeadingChar.DenyZero:
# TOML specifications forbid this
var upcomingChar = state.getNextChar()
if upcomingChar in Digits:
raise(newTomlError(state,
"leading zeroes are not allowed in integers"))
else:
state.pushBackChar(upcomingChar)
if nextChar notin digits:
if wasUnderscore:
raise(newTomlError(state,
"underscore must be surrounded by digit"))
state.pushBackChar(nextChar)
break
try:
result = result * baseNum - charToInt(nextChar, base)
except OverflowDefect:
raise(newTomlError(state,
"integer numbers wider than 64 bits not allowed"))
firstPos = false
if not negative:
try:
result = -result
except OverflowDefect:
raise(newTomlError(state,
"integer numbers wider than 64 bits not allowed"))
proc parseEncoding(state: var ParserState): TomlValueRef =
let nextChar = state.getNextChar()
case nextChar:
of 'b':
return TomlValueRef(kind: TomlValueKind.Int, intVal: parseInt(state, base2, LeadingChar.AllowZero))
of 'o':
return TomlValueRef(kind: TomlValueKind.Int, intVal: parseInt(state, base8, LeadingChar.AllowZero))
of 'x':
return TomlValueRef(kind: TomlValueKind.Int, intVal: parseInt(state, base16, LeadingChar.AllowZero))
else: raise newTomlError(state, "illegal character")
proc parseDecimalPart(state: var ParserState): float64 =
var
nextChar: char
firstPos = true
wasUnderscore = false
decimalPartStr = "0."
while true:
wasUnderscore = nextChar == '_'
nextChar = state.getNextChar()
if nextChar == '_':
if firstPos or wasUnderscore:
raise(newTomlError(state,
"underscore must be surrounded by digit"))
continue
if nextChar notin strutils.Digits:
if wasUnderscore:
raise(newTomlError(state,
"underscore must be surrounded by digit"))
state.pushBackChar(nextChar)
if firstPos:
raise newTomlError(state, "decimal part empty")
break
decimalPartStr.add(nextChar)
firstPos = false
doAssert decimalPartStr.len > 2 # decimalPartStr shouldn't still be "0." at this point
discard parseutils.parseFloat(decimalPartStr, result)
proc stringDelimiter(kind: StringType): char {.inline, noSideEffect.} =
result = (case kind
of StringType.Basic: '\"'
of StringType.Literal: '\'')
proc parseUnicode(state: var ParserState): string =
let
escapeKindChar = state.getNextChar()
oldState = (column: state.column, line: state.line)
code = parseInt(state, base16, LeadingChar.AllowZero)
if state.line != oldState.line:
raise newTomlError(state, "invalid Unicode codepoint, can't span lines")
if escapeKindChar == 'u' and state.column - 5 != oldState.column:
raise newTomlError(state, "invalid Unicode codepoint, 'u' must have " &
"four character value")
if escapeKindChar == 'U' and state.column - 9 != oldState.column:
raise newTomlError(state, "invalid Unicode codepoint, 'U' must have " &
"eight character value")
if code notin 0'i64..0xD7FF and code notin 0xE000'i64..0x10FFFF:
raise(newTomlError(state, "invalid Unicode codepoint, " &
"must be a Unicode scalar value"))
return unicode.toUTF8(Rune(code))
proc parseEscapeChar(state: var ParserState, escape: char): string =
case escape
of 'b': result = "\b"
of 't': result = "\t"
of 'n': result = "\l"
of 'f': result = "\f"
of 'r': result = "\r"
of '\'': result = "\'"
of '\"': result = "\""
of '\\': result = "\\"
of 'u', 'U':
state.pushBackChar(escape)
result = parseUnicode(state)
else:
raise(newTomlError(state,
"unknown escape " &
"sequence \"\\" & escape & "\""))
proc parseSingleLineString(state: var ParserState, kind: StringType): string =
# This procedure parses strings enclosed within single/double
# quotation marks. It assumes that the quotation mark has already
# been consumed by the "state" variable, which therefore is ready
# to read the first character of the string.
result = newStringOfCap(defaultStringCapacity)
let delimiter = stringDelimiter(kind)
var nextChar: char
while true:
nextChar = state.getNextChar()
if nextChar == delimiter:
break
if nextChar == '\0':
raise(newTomlError(state, "unterminated string"))
# https://toml.io/en/v1.0.0#string
# Any Unicode character may be used except those that must be escaped:
# quotation mark, backslash, and the control characters other than tab
# (U+0000 to U+0008, U+000A to U+001F, U+007F).
if nextChar in ctrlChars:
raise(newTomlError(state, "invalid character in string: 0x$#" % nextChar.ord.toHex(2)))
if nextChar == '\\' and kind == StringType.Basic:
nextChar = state.getNextChar()
result.add(state.parseEscapeChar(nextChar))
continue
result.add(nextChar)
proc parseMultiLineString(state: var ParserState, kind: StringType): string =
# This procedure parses strings enclosed within three consecutive
# sigle/double quotation marks. It assumes that all the quotation
# marks have already been consumed by the "state" variable, which
# therefore is ready to read the first character of the string.
result = newStringOfCap(defaultStringCapacity)
let delimiter = stringDelimiter(kind)
var
isFirstChar = true
nextChar: char
while true:
nextChar = state.getNextChar()
# Skip the first newline, if it comes immediately after the
# quotation marks
if isFirstChar and (nextChar == '\l'):
isFirstChar = false
continue
if nextChar == delimiter:
# Are we done?
nextChar = state.getNextChar()
if nextChar == delimiter:
nextChar = state.getNextChar()
if nextChar == delimiter:
# Done with this string
return
else:
# Just got a double delimiter
result.add(delimiter & delimiter)
state.pushBackChar(nextChar)
continue
else:
# Just got a lone delimiter
result.add(delimiter)
state.pushBackChar(nextChar)
continue
if nextChar == '\\' and kind == StringType.Basic:
# This can either be an escape sequence or a end-of-line char
nextChar = state.getNextChar()
if nextChar in {'\l', '\r', ' '}:
# We're at the end of a line: skip everything till the
# next non-whitespace character
while nextChar in {'\l', '\r', ' ', '\t'}:
nextChar = state.getNextChar()
state.pushBackChar(nextChar)
continue
else:
# This is just an escape sequence (like "\t")
#nextChar = state.getNextChar()
result.add(state.parseEscapeChar(nextChar))
continue
if nextChar == '\0':
raise(newTomlError(state, "unterminated string"))
# https://toml.io/en/v1.0.0#string
# Any Unicode character may be used except those that must be
# escaped: backslash and the control characters other than tab,
# line feed, and carriage return (U+0000 to U+0008, U+000B,
# U+000C, U+000E to U+001F, U+007F).
if nextChar in ctrlCharsExclCrLf:
raise(newTomlError(state, "invalid character in string: 0x$#" % nextChar.ord.toHex(2)))
result.add(nextChar)
isFirstChar = false
proc parseString(state: var ParserState, kind: StringType): string =
## This function assumes that "state" has already consumed the
## first character (either \" or \', which is passed in the
## "openChar" parameter).
let delimiter = stringDelimiter(kind)
var nextChar: char = state.getNextChar()
if nextChar == delimiter:
# We have two possibilities here: (1) the empty string, or (2)
# "long" multi-line strings.
nextChar = state.getNextChar()
if nextChar == delimiter:
return parseMultiLineString(state, kind)
else:
# Empty string. This was easy!
state.pushBackChar(nextChar)
return ""
else:
state.pushBackChar(nextChar)
return parseSingleLineString(state, kind)
# Forward declaration
proc parseValue(state: var ParserState): TomlValueRef
proc parseInlineTable(state: var ParserState): TomlValueRef
proc parseArray(state: var ParserState): seq[TomlValueRef] =
# This procedure assumes that "state" has already consumed the '['
# character
result = newSeq[TomlValueRef](0)
while true:
var nextChar: char = state.getNextNonWhitespace(skipLf)
case nextChar
of ']':
return
of ',':
if len(result) == 0:
# This happens with "[, 1, 2]", for instance
raise(newTomlError(state, "first array element missing"))
# Check that this is not a terminating comma (like in
# "[b,]")
nextChar = state.getNextNonWhitespace(skipLf)
if nextChar == ']':
return
state.pushBackChar(nextChar)
else:
let oldState = state # Saved for error messages
var newValue: TomlValueRef
if nextChar != '{':
state.pushBackChar(nextChar)
newValue = parseValue(state)
else:
newValue = parseInlineTable(state)
if len(result) > 0:
# Check that the type of newValue is compatible with the
# previous ones
if newValue.kind != result[low(result)].kind:
raise(newTomlError(oldState,
"array members with incompatible types"))
result.add(newValue)
proc parseStrictNum(state: var ParserState,
minVal: int,
maxVal: int,
count: Slice[int],
msg: string): int =
var
nextChar: char
parsedChars = 0
result = 0
while true:
nextChar = state.getNextChar()
if nextChar notin strutils.Digits:
state.pushBackChar(nextChar)
break
try:
result = result * 10 + charToInt(nextChar, base10)
parsedChars += 1
except OverflowDefect:
raise(newTomlError(state,
"integer numbers wider than 64 bits not allowed"))
if parsedChars notin count:
raise(newTomlError(state,
"too few or too many characters in digit, expected " &
$count & " got " & $parsedChars))
if result < minVal or result > maxVal:
raise(newTomlError(state, msg & " (" & $result & ")"))
template parseStrictNum(state: var ParserState,
minVal: int,
maxVal: int,
count: int,
msg: string): int =
parseStrictNum(state, minVal, maxVal, (count..count), msg)
proc parseTimePart(state: var ParserState, val: var TomlTime) =
var
nextChar: char
curLine = state.line
# Parse the minutes
val.minute = parseStrictNum(state, minVal = 0, maxVal = 59, count = 2,
"number out of range for minutes")
if curLine != state.line:
raise(newTomlError(state, "invalid date field, stopped in or after minutes field"))
nextChar = state.getNextChar()
if nextChar != ':':
raise(newTomlError(state,
"\":\" expected after the number of seconds"))
# Parse the second. Note that seconds=60 *can* happen (leap second)
val.second = parseStrictNum(state, minVal = 0, maxVal = 60, count = 2,
"number out of range for seconds")
nextChar = state.getNextChar()
if nextChar == '.':
val.subsecond = parseInt(state, base10, LeadingChar.AllowZero).int
else:
state.pushBackChar(nextChar)
proc parseDateTimePart(state: var ParserState,
dateTime: var TomlDateTime): bool =
# This function is called whenever a datetime object is found. They follow
# an ISO convention and can use one of the following format:
#
# - YYYY-MM-DDThh:mm:ss[+-]hh:mm
# - YYYY-MM-DDThh:mm:ssZ
#
# where the "T" and "Z" letters are literals, [+-] indicates
# *either* "+" or "-", YYYY is the 4-digit year, MM is the 2-digit
# month, DD is the 2-digit day, hh is the 2-digit hour, mm is the
# 2-digit minute, and ss is the 2-digit second. The hh:mm after
# the +/- character is the timezone; a literal "Z" indicates the
# local timezone.
# This function assumes that the "YYYY-" part has already been
# parsed (this happens because during parsing, finding a 4-digit
# number like "YYYY" might just indicate the presence of an
# integer or a floating-point number; it's the following "-" that
# tells the parser that the value is a datetime). As a consequence
# of this, we assume that "dateTime.year" has already been set.
var
nextChar: char
curLine = state.line
# Parse the month
dateTime.date.month = parseStrictNum(state, minVal = 1, maxVal = 12, count = 2,
"number out of range for the month")
if curLine != state.line:
raise(newTomlError(state, "invalid date field, stopped in or after month field"))
nextChar = state.getNextChar()
if nextChar != '-':
raise(newTomlError(state, "\"-\" expected after the month number"))
# Parse the day
dateTime.date.day = parseStrictNum(state, minVal = 1, maxVal = 31, count = 2,
"number out of range for the day")
if curLine != state.line:
return false
else:
nextChar = state.getNextChar()
if nextChar notin {'t', 'T', ' '}:
raise(newTomlError(state, "\"T\", \"t\", or space expected after the day number"))
# Parse the hour
dateTime.time.hour = parseStrictNum(state, minVal = 0, maxVal = 23, count = 2,
"number out of range for the hours")
if curLine != state.line:
raise(newTomlError(state, "invalid date field, stopped in or after hours field"))
nextChar = state.getNextChar()
if nextChar != ':':
raise(newTomlError(state, "\":\" expected after the number of hours"))
# Parse the minutes
dateTime.time.minute = parseStrictNum(state, minVal = 0, maxVal = 59, count = 2,
"number out of range for minutes")
if curLine != state.line:
raise(newTomlError(state, "invalid date field, stopped in or after minutes field"))
nextChar = state.getNextChar()
if nextChar != ':':
raise(newTomlError(state,
"\":\" expected after the number of seconds"))
# Parse the second. Note that seconds=60 *can* happen (leap second)
dateTime.time.second = parseStrictNum(state, minVal = 0, maxVal = 60, count = 2,
"number out of range for seconds")
nextChar = state.getNextChar()
if nextChar == '.':
dateTime.time.subsecond = parseInt(state, base10, LeadingChar.AllowZero).int
else:
state.pushBackChar(nextChar)
nextChar = state.getNextChar()
case nextChar
of 'z', 'Z':
dateTime = TomlDateTime(
time: dateTime.time,
date: dateTime.date,
shift: true,
isShiftPositive: true,
zoneHourShift: 0,
zoneMinuteShift: 0
)
of '+', '-':
dateTime = TomlDateTime(
time: dateTime.time,
date: dateTime.date,
shift: true,
isShiftPositive: (nextChar == '+')
)
dateTime.zoneHourShift =
parseStrictNum(state, minVal = 0, maxVal = 23, count = 2,
"number out of range for shift hours")
if curLine != state.line:
raise(newTomlError(state, "invalid date field, stopped in or after shift hours field"))
nextChar = state.getNextChar()
if nextChar != ':':
raise(newTomlError(state,
"\":\" expected after the number of shift hours"))
dateTime.zoneMinuteShift =
parseStrictNum(state, minVal = 0, maxVal = 59, count = 2,
"number out of range for shift minutes")
else:
if curLine == state.line:
raise(newTomlError(state, "unexpected character " & escape($nextChar) &
" instead of the time zone"))
else: # shift is automatically initialized to false
state.pushBackChar(nextChar)
return true
proc parseDateOrTime(state: var ParserState, digits: int, yearOrHour: int): TomlValueRef =
var
nextChar: char
yoh = yearOrHour
d = digits
while true:
nextChar = state.getNextChar()
case nextChar:
of ':':
if d != 2:
raise newTomlError(state, "wrong number of characters for hour")
var val: TomlTime
val.hour = yoh
parseTimePart(state, val)
return TomlValueRef(kind: TomlValueKind.Time, timeVal: val)
of '-':
if d != 4:
raise newTomlError(state, "wrong number of characters for year")
var val: TomlDateTime
val.date.year = yoh
let fullDate = parseDateTimePart(state, val)
if fullDate:
return TomlValueRef(kind: TomlValueKind.DateTime,
dateTimeVal: val)
else:
return TomlValueRef(kind: TomlValueKind.Date,
dateVal: val.date)
of strutils.Digits:
if d == 4:
raise newTomlError(state, "leading zero not allowed")
try:
yoh *= 10
yoh += ord(nextChar) - ord('0')
d += 1
except OverflowDefect:
raise newTomlError(state, "number larger than 64 bits wide")
continue
of strutils.Whitespace:
raise newTomlError(state, "leading zero not allowed")
else: raise newTomlError(state, "illegal character")
break
proc parseFloat(state: var ParserState, intPart: int, forcedSign: Sign): TomlValueRef =
var
decimalPart = parseDecimalPart(state)
nextChar = state.getNextChar()
exponent: int64 = 0
if nextChar in {'e', 'E'}:
exponent = parseInt(state, base10, LeadingChar.AllowZero)
else:
state.pushBackChar(nextChar)
let value =
if intPart <= 0:
pow(10.0, exponent.float64) * (float64(intPart) - decimalPart)
else:
pow(10.0, exponent.float64) * (float64(intPart) + decimalPart)
return TomlValueRef(kind: TomlValueKind.Float, floatVal: if forcedSign != Neg: -value else: value, forcedSign: forcedSign)
proc parseNumOrDate(state: var ParserState): TomlValueRef =
var
nextChar: char
forcedSign: Sign = None
while true:
nextChar = state.getNextChar()
case nextChar:
of '0':
nextChar = state.getNextChar()
if forcedSign == None:
if nextChar in {'b', 'x', 'o'}:
state.pushBackChar(nextChar)
return parseEncoding(state)
else:
# This must now be a float or a date/time, or a sole 0
case nextChar:
of '.':
return parseFloat(state, 0, forcedSign)
of strutils.Whitespace:
state.pushBackChar(nextChar)
return TomlValueRef(kind: TomlValueKind.Int, intVal: 0)
of strutils.Digits:
# This must now be a date/time
return parseDateOrTime(state, digits = 2, yearOrHour = ord(nextChar) - ord('0'))
else:
# else is a sole 0
return TomlValueRef(kind: TomlValueKind.Int, intVal: 0)
else:
# This must now be a float, or a sole 0
case nextChar:
of '.':
return parseFloat(state, 0, forcedSign)
of strutils.Whitespace:
state.pushBackChar(nextChar)
return TomlValueRef(kind: TomlValueKind.Int, intVal: 0)
else:
# else is a sole 0
return TomlValueRef(kind: TomlValueKind.Int, intVal: 0)
of strutils.Digits - {'0'}:
# This might be a date/time, or an int or a float
var
digits = 1
curSum = ord('0') - ord(nextChar)
wasUnderscore = false
while true:
nextChar = state.getNextChar()
if wasUnderscore and nextChar notin strutils.Digits:
raise newTomlError(state, "underscores must be surrounded by digits")
case nextChar:
of ':':
if digits != 2:
raise newTomlError(state, "wrong number of characters for hour")
var val: TomlTime
val.hour = -curSum
parseTimePart(state, val)
return TomlValueRef(kind: TomlValueKind.Time, timeVal: val)
of '-':
if digits != 4:
raise newTomlError(state, "wrong number of characters for year")
var val: TomlDateTime
val.date.year = -curSum
let fullDate = parseDateTimePart(state, val)
if fullDate:
return TomlValueRef(kind: TomlValueKind.DateTime,
dateTimeVal: val)
else:
return TomlValueRef(kind: TomlValueKind.Date,
dateVal: val.date)
of '.':
return parseFloat(state, curSum, forcedSign)
of 'e', 'E':
var exponent = parseInt(state, base10, LeadingChar.AllowZero)
let value = pow(10.0, exponent.float64) * float64(curSum)
return TomlValueRef(kind: TomlValueKind.Float, floatVal: if forcedSign != Neg: -value else: value)
of strutils.Digits:
try:
curSum *= 10
curSum += ord('0') - ord(nextChar)
digits += 1
except OverflowDefect:
raise newTomlError(state, "number larger than 64 bits wide")
wasUnderscore = false
continue
of '_':
wasUnderscore = true
continue
of strutils.Whitespace:
state.pushBackChar(nextChar)
return TomlValueRef(kind: TomlValueKind.Int, intVal: if forcedSign != Neg: -curSum else: curSum)
else:
state.pushBackChar(nextChar)
return TomlValueRef(kind: TomlValueKind.Int, intVal: if forcedSign != Neg: -curSum else: curSum)
break
of '+', '-':
forcedSign = if nextChar == '+': Pos else: Neg
continue
of 'i':
# Is this "inf"?
let oldState = state
if state.getNextChar() != 'n' or
state.getNextChar() != 'f':
raise(newTomlError(oldState, "unknown identifier"))
return TomlValueRef(kind: TomlValueKind.Float, floatVal: if forcedSign == Neg: NegInf else: Inf, forcedSign: forcedSign)
of 'n':
# Is this "nan"?
let oldState = state
if state.getNextChar() != 'a' or
state.getNextChar() != 'n':
raise(newTomlError(oldState, "unknown identifier"))
return TomlValueRef(kind: TomlValueKind.Float, floatVal: NaN, forcedSign: forcedSign)
else:
raise newTomlError(state, "illegal character " & escape($nextChar))
break
proc parseValue(state: var ParserState): TomlValueRef =
var nextChar: char
nextChar = state.getNextNonWhitespace(skipNoLf)
case nextChar
of strutils.Digits, '+', '-', 'i', 'n':
state.pushBackChar(nextChar)
return parseNumOrDate(state)
of 't':
# Is this "true"?
let oldState = state # Only used for error messages
if state.getNextChar() != 'r' or
state.getNextChar() != 'u' or
state.getNextChar() != 'e':
raise(newTomlError(oldState, "unknown identifier"))
result = TomlValueRef(kind: TomlValueKind.Bool, boolVal: true)
of 'f':
# Is this "false"?
let oldState = state # Only used for error messages
if state.getNextChar() != 'a' or
state.getNextChar() != 'l' or
state.getNextChar() != 's' or
state.getNextChar() != 'e':
raise(newTomlError(oldState, "unknown identifier"))
result = TomlValueRef(kind: TomlValueKind.Bool, boolVal: false)
of '\"':
# A basic string (accepts \ escape codes)
result = TomlValueRef(kind: TomlValueKind.String,
stringVal: parseString(state, StringType.Basic))
of '\'':
# A literal string (does not accept \ escape codes)
result = TomlValueRef(kind: TomlValueKind.String,
stringVal: parseString(state, StringType.Literal))
of '[':
# An array
result = TomlValueRef(kind: TomlValueKind.Array,
arrayVal: parseArray(state))
else:
raise(newTomlError(state,
"unexpected character " & escape($nextChar)))
proc parseName(state: var ParserState): string =
# This parses the name of a key or a table
result = newStringOfCap(defaultStringCapacity)
var nextChar = state.getNextNonWhitespace(skipNoLf)
if nextChar == '\"':
return state.parseString(StringType.Basic)
elif nextChar == '\'':
return state.parseString(StringType.Literal)
state.pushBackChar(nextChar)
while true:
nextChar = state.getNextChar()
if (nextChar in {'=', '.', '[', ']', '\0', ' ', '\t'}):
# Any of the above characters marks the end of the name
state.pushBackChar(nextChar)
break
elif (nextChar notin {'a'..'z', 'A'..'Z', '0'..'9', '_', '-'}):
raise(newTomlError(state,
"bare key has illegal character: " & escape($nextChar)))
else:
result.add(nextChar)
type
BracketType {.pure.} = enum
single, double
proc parseTableName(state: var ParserState,
brackets: BracketType): seq[string] =
# This code assumes that '[' has already been consumed
result = newSeq[string](0)
while true:
#let partName = state.parseName(SpecialChars.AllowNumberSign)
var
nextChar = state.getNextChar()
partName: string
if nextChar == '"':
partName = state.parseString(StringType.Basic)
else:
state.pushBackChar(nextChar)
partName = state.parseName()
result.add(partName)
nextChar = state.getNextNonWhitespace(skipNoLf)
case nextChar
of ']':
if brackets == BracketType.double:
nextChar = state.getNextChar()
if nextChar != ']':
raise(newTomlError(state,
"\"]]\" expected"))
# We must check that there is nothing else in this line
nextChar = state.getNextNonWhitespace(skipNoLf)
if nextChar notin {'\l', '\0'}:
raise(newTomlError(state,
"unexpected character " & escape($nextChar)))
break
of '.': continue
else:
raise(newTomlError(state,
"unexpected character " & escape($nextChar)))
proc setEmptyTableVal(val: var TomlValueRef) =
val = TomlValueRef(kind: TomlValueKind.Table)
new(val.tableVal)
val.tableVal[] = initOrderedTable[string, TomlValueRef]()
proc parseInlineTable(state: var ParserState): TomlValueRef =
new(result)
setEmptyTableVal(result)
var firstComma = true
while true:
var nextChar = state.getNextNonWhitespace(skipNoLf)
case nextChar
of '}':
return
of ',':
if firstComma:
raise(newTomlError(state, "first inline table element missing"))
# Check that this is not a terminating comma (like in
# "[b,]")
nextChar = state.getNextNonWhitespace(skipNoLf)
if nextChar == '}':
return
state.pushBackChar(nextChar)
of '\n':
raise(newTomlError(state, "inline tables cannot contain newlines"))
else:
firstComma = false
state.pushBackChar(nextChar)
var key = state.parseName()
nextChar = state.getNextNonWhitespace(skipNoLf)
var curTable = result.tableVal
while nextChar == '.':
var deepestTable = new(TomlTableRef)
deepestTable[] = initOrderedTable[string, TomlValueRef]()
curTable[key] = TomlValueRef(kind: TomlValueKind.Table, tableVal: deepestTable)
curTable = deepestTable
key = state.parseName()
nextChar = state.getNextNonWhitespace(skipNoLf)
if nextChar != '=':
raise(newTomlError(state,
"key names cannot contain spaces"))
nextChar = state.getNextNonWhitespace(skipNoLf)
if nextChar == '{':
curTable[key] = state.parseInlineTable()
else:
state.pushBackChar(nextChar)
curTable[key] = state.parseValue()
proc createTableDef(state: var ParserState,
tableNames: seq[string],
dotted = false)
proc parseKeyValuePair(state: var ParserState) =
var
tableKeys: seq[string]
key: string
nextChar: char
oldTableRef = state.curTableRef
while true:
let subkey = state.parseName()
nextChar = state.getNextNonWhitespace(skipNoLf)
if nextChar == '.':
tableKeys.add subkey
else:
if tableKeys.len != 0:
createTableDef(state, tableKeys, dotted = true)
key = subkey
break
if nextChar != '=':
raise(newTomlError(state,
"key names cannot contain character \"" & nextChar & "\""))
nextChar = state.getNextNonWhitespace(skipNoLf)
# Check that this is a regular value and not an inline table
if nextChar != '{':
state.pushBackChar(nextChar)
let value = state.parseValue()
# We must check that there is nothing else in this line
nextChar = state.getNextNonWhitespace(skipNoLf)
if nextChar != '\l' and nextChar != '\0':
raise(newTomlError(state,
"unexpected character " & escape($nextChar)))
if state.curTableRef.hasKey(key):
raise(newTomlError(state,
"duplicate key, \"" & key & "\" already in table"))
state.curTableRef[key] = value
else:
#createTableDef(state, @[key])
if key.len == 0:
raise newTomlError(state, "empty key not allowed")
if state.curTableRef.hasKey(key):
raise newTomlError(state, "duplicate table key not allowed")
state.curTableRef[key] = parseInlineTable(state)
state.curTableRef = oldTableRef
proc newParserState(s: streams.Stream,
fileName: string = ""): ParserState =
result = ParserState(fileName: fileName, line: 1, column: 1, stream: s)
proc setArrayVal(val: var TomlValueRef, numOfElems: int = 0) =
val = TomlValueRef(kind: TomlValueKind.Array)
val.arrayVal = newSeq[TomlValueRef](numOfElems)
proc advanceToNextNestLevel(state: var ParserState,
tableName: string) =
let target = state.curTableRef[tableName]
case target.kind
of TomlValueKind.Table:
state.curTableRef = target.tableVal
of TomlValueKind.Array:
let arr = target.arrayVal[high(target.arrayVal)]
if arr.kind != TomlValueKind.Table:
raise(newTomlError(state, "\"" & tableName &
"\" elements are not tables"))
state.curTableRef = arr.tableVal
else:
raise(newTomlError(state, "\"" & tableName &
"\" is not a table"))
# This function is called by the TOML parser whenever a
# "[[table.name]]" line is encountered in the parsing process. Its
# purpose is to make sure that all the parent nodes in "table.name"
# exist and are tables, and that a terminal node of the correct type
# is created.
#
# Starting from "curTableRef" (which is usually the root object),
# traverse the object tree following the names in "tableNames" and
# create a new TomlValueRef object of kind "TomlValueKind.Array" at
# the terminal node. This array is going to be an array of tables: the
# function will create an element and will make "curTableRef"
# reference it. Example: if tableNames == ["a", "b", "c"], the code
# will look for the "b" table that is child of "a", and then it will
# check if "c" is a child of "b". If it is, it must be an array of
# tables, and a new element will be appended. Otherwise, a new "c"
# array is created, and an empty table element is added in "c". In
# either cases, curTableRef will refer to the last element of "c".
proc createOrAppendTableArrayDef(state: var ParserState,
tableNames: seq[string]) =
# This is a table array entry (e.g. "[[entry]]")
for idx, tableName in tableNames:
if tableName.len == 0:
raise(newTomlError(state,
"empty key not allowed"))
let lastTableInChain = idx == high(tableNames)
var newValue: TomlValueRef
if not state.curTableRef.hasKey(tableName):
# If this element does not exist, create it
new(newValue)
# If this is the last name in the chain (e.g.,
# "c" in "a.b.c"), its value should be an
# array of tables, otherwise just a table
if lastTableInChain:
setArrayVal(newValue, 1)
new(newValue.arrayVal[0])
setEmptyTableVal(newValue.arrayVal[0])
state.curTableRef[tableName] = newValue
state.curTableRef = newValue.arrayVal[0].tableVal
else:
setEmptyTableVal(newValue)
# Add the newly created object to the current table
state.curTableRef[tableName] = newValue
# Update the pointer to the current table
state.curTableRef = newValue.tableVal
else:
# The element exists: is it of the right type?
let target = state.curTableRef[tableName]
if lastTableInChain:
if target.kind != TomlValueKind.Array:
raise(newTomlError(state, "\"" & tableName &
"\" is not an array"))
var newValue: TomlValueRef
new(newValue)
setEmptyTableVal(newValue)
target.arrayVal.add(newValue)
state.curTableRef = newValue.tableVal
else:
advanceToNextNestLevel(state, tableName)
# Starting from "curTableRef" (which is usually the root object),
# traverse the object tree following the names in "tableNames" and
# create a new TomlValueRef object of kind "TomlValueKind.Table" at
# the terminal node. Example: if tableNames == ["a", "b", "c"], the
# code will look for the "b" table that is child of "a" and it will
# create a new table "c" which is "b"'s children.
proc createTableDef(state: var ParserState,
tableNames: seq[string],
dotted = false) =
var newValue: TomlValueRef
# This starts a new table (e.g. "[table]")
for i, tableName in tableNames:
if tableName.len == 0:
raise(newTomlError(state,
"empty key not allowed"))
if not state.curTableRef.hasKey(tableName):
new(newValue)
setEmptyTableVal(newValue)
# Add the newly created object to the current table
state.curTableRef[tableName] = newValue
# Update the pointer to the current table
state.curTableRef = newValue.tableVal
else:
if i == tableNames.high and state.curTableRef.hasKey(tableName) and
state.curTableRef[tableName].kind == TomlValueKind.Table:
if state.curTableRef[tableName].tableVal.len == 0:
raise newTomlError(state, "duplicate table key not allowed")
elif not dotted:
for value in state.curTableRef[tableName].tableVal.values:
if value.kind != TomlValueKind.Table:
raise newTomlError(state, "duplicate table key not allowed")
advanceToNextNestLevel(state, tableName)
proc parseStream*(inputStream: streams.Stream,
fileName: string = ""): TomlValueRef =
## Parses a stream of TOML formatted data into a TOML table. The optional
## filename is used for error messages.
if inputStream == nil:
raise newException(IOError,
"Unable to read from the stream created from: \"" & fileName & "\", " &
"possibly a missing file")
var state = newParserState(inputStream, fileName)
result = TomlValueRef(kind: TomlValueKind.Table)
new(result.tableVal)
result.tableVal[] = initOrderedTable[string, TomlValueRef]()
# This pointer will always point to the table that should get new
# key/value pairs found in the TOML file during parsing
state.curTableRef = result.tableVal
# Unlike "curTableRef", this pointer never changes: it always
# points to the uppermost table in the tree
let baseTable = result.tableVal
var nextChar: char
while true:
nextChar = state.getNextNonWhitespace(skipLf)
case nextChar
of '[':
# A new section/table begins. We'll have to start again
# from the uppermost level, so let's rewind curTableRef to
# the root node
state.curTableRef = baseTable
# First, decompose the table name into its part (e.g.,
# "a.b.c" -> ["a", "b", "c"])
nextChar = state.getNextChar()
let isTableArrayDef = nextChar == '['
var tableNames: seq[string]
if isTableArrayDef:
tableNames = state.parseTableName(BracketType.double)
else:
state.pushBackChar(nextChar)
tableNames = state.parseTableName(BracketType.single)
# Now create the proper (empty) data structure: either a
# table or an array of tables. Note that both functions
# update the "curTableRef" variable: they have to, since
# the TOML specification says that any "key = value"
# statement that follows is a child of the table we're
# defining right now, and we use "curTableRef" as a
# reference to the table that gets every next key/value
# definition.
if isTableArrayDef:
createOrAppendTableArrayDef(state, tableNames)
else:
createTableDef(state, tableNames)
of '=':
raise(newTomlError(state, "key name missing"))
of '#', '.', ']':
raise(newTomlError(state,
"unexpected character " & escape($nextChar)))
of '\0': # EOF
return
else:
# Everything else marks the presence of a "key = value" pattern
state.pushBackChar(nextChar)
parseKeyValuePair(state)
proc parseString*(tomlStr: string, fileName: string = ""): TomlValueRef =
## Parses a string of TOML formatted data into a TOML table. The optional
## filename is used for error messages.
let strStream = newStringStream(tomlStr)
try:
result = parseStream(strStream, fileName)
finally:
strStream.close()
proc parseFile*(f: File, fileName: string = ""): TomlValueRef =
## Parses a file of TOML formatted data into a TOML table. The optional
## filename is used for error messages.
let fStream = newFileStream(f)
try:
result = parseStream(fStream, fileName)
finally:
fStream.close()
proc parseFile*(fileName: string): TomlValueRef =
## Parses the file found at fileName with TOML formatted data into a TOML
## table.
let fStream = newFileStream(fileName, fmRead)
if not isNil(fStream):
try:
result = parseStream(fStream, fileName)
finally:
fStream.close()
else:
raise newException(IOError, "cannot open: " & fileName)
proc `$`*(val: TomlDate): string =
## Converts the TOML date object into the ISO format read by the parser
result = ($val.year).align(4, '0') & "-" & ($val.month).align(2, '0') & "-" &
($val.day).align(2, '0')
proc `$`*(val: TomlTime): string =
## Converts the TOML time object into the ISO format read by the parser
result = ($val.hour).align(2, '0') & ":" &
($val.minute).align(2, '0') & ":" & ($val.second).align(2, '0') &
(if val.subsecond > 0: ("." & $val.subsecond) else: "")
proc `$`*(val: TomlDateTime): string =
## Converts the TOML date-time object into the ISO format read by the parser
result = $val.date & "T" & $val.time &
(if not val.shift: "" else: (
(if val.zoneHourShift == 0 and val.zoneMinuteShift == 0: "Z" else: (
((if val.isShiftPositive: "+" else: "-") &
($val.zoneHourShift).align(2, '0') & ":" &
($val.zoneMinuteShift).align(2, '0'))
))
))
proc toTomlString*(value: TomlValueRef): string
proc `$`*(val: TomlValueRef): string =
## Turns whatever value into a regular Nim value representtation
case val.kind
of TomlValueKind.None:
result = "nil"
of TomlValueKind.Int:
result = $val.intVal
of TomlValueKind.Float:
result = $val.floatVal
of TomlValueKind.Bool:
result = $val.boolVal
of TomlValueKind.Datetime:
result = $val.dateTimeVal
of TomlValueKind.Date:
result = $val.dateVal
of TomlValueKind.Time:
result = $val.timeVal
of TomlValueKind.String:
result = $val.stringVal
of TomlValueKind.Array:
result = ""
for elem in val.arrayVal:
result.add($(elem[]))
of TomlValueKind.Table:
result = val.toTomlString
proc `$`*(val: TomlValue): string =
## Turns whatever value into a type and value representation, used by ``dump``
case val.kind
of TomlValueKind.None:
result = "none()"
of TomlValueKind.Int:
result = "int(" & $val.intVal & ")"
of TomlValueKind.Float:
result = "float(" & $val.floatVal & ")"
of TomlValueKind.Bool:
result = "boolean(" & $val.boolVal & ")"
of TomlValueKind.Datetime:
result = "datetime(" & $val.dateTimeVal & ")"
of TomlValueKind.Date:
result = "date(" & $val.dateVal & ")"
of TomlValueKind.Time:
result = "time(" & $val.timeVal & ")"
of TomlValueKind.String:
result = "string(\"" & $val.stringVal & "\")"
of TomlValueKind.Array:
result = "array("
for elem in val.arrayVal:
result.add($(elem[]))
result.add(")")
of TomlValueKind.Table:
result = "table(" & $(len(val.tableVal)) & " elements)"
proc dump*(table: TomlTableRef, indentLevel: int = 0) =
## Dump out the entire table as it was parsed. This procedure is mostly
## useful for debugging purposes
let space = spaces(indentLevel)
for key, val in pairs(table):
if val.kind == TomlValueKind.Table:
echo space & key & " = table"
dump(val.tableVal, indentLevel + 4)
elif (val.kind == TomlValueKind.Array and
val.arrayVal[0].kind == TomlValueKind.Table):
for idx, val in val.arrayVal:
echo space & key & "[" & $idx & "] = table"
dump(val.tableVal, indentLevel + 4)
else:
echo space & key & " = " & $(val[])
import json, sequtils
proc toJson*(value: TomlValueRef): JsonNode
proc toJson*(table: TomlTableRef): JsonNode =
## Converts a TOML table to a JSON node. This uses the format specified in
## the validation suite for it's output:
## https://github.com/BurntSushi/toml-test#example-json-encoding
result = newJObject()
for key, value in pairs(table):
result[key] = value.toJson
proc toJson*(value: TomlValueRef): JsonNode =
## Converts a TOML value to a JSON node. This uses the format specified in
## the validation suite for it's output:
## https://github.com/BurntSushi/toml-test#example-json-encoding
case value.kind:
of TomlValueKind.Int:
%*{"type": "integer", "value": $value.intVal}
of TomlValueKind.Float:
if classify(value.floatVal) == fcNan:
if value.forcedSign != Pos:
%*{"type": "float", "value": $value.floatVal}
else:
%*{"type": "float", "value": "+" & $value.floatVal}
else:
%*{"type": "float", "value": $value.floatVal}
of TomlValueKind.Bool:
%*{"type": "bool", "value": $value.boolVal}
of TomlValueKind.Datetime:
if value.dateTimeVal.shift == false:
%*{"type": "datetime-local", "value": $value.dateTimeVal}
else:
%*{"type": "datetime", "value": $value.dateTimeVal}
of TomlValueKind.Date:
%*{"type": "date", "value": $value.dateVal}
of TomlValueKind.Time:
%*{"type": "time", "value": $value.timeVal}
of TomlValueKind.String:
%*{"type": "string", "value": newJString(value.stringVal)}
of TomlValueKind.Array:
if value.arrayVal.len == 0:
when defined(newtestsuite):
%[]
else:
%*{"type": "array", "value": []}
elif value.arrayVal[0].kind == TomlValueKind.Table:
%value.arrayVal.map(toJson)
else:
when defined(newtestsuite):
%*value.arrayVal.map(toJson)
else:
%*{"type": "array", "value": value.arrayVal.map(toJson)}
of TomlValueKind.Table:
value.tableVal.toJson
of TomlValueKind.None:
%*{"type": "ERROR"}
proc toKey(str: string): string =
for c in str:
if (c notin {'a'..'z', 'A'..'Z', '0'..'9', '_', '-'}):
return "\"" & str & "\""
str
proc toTomlString*(value: TomlTableRef, parents = ""): string =
## Converts a TOML table to a TOML formatted string for output to a file.
result = ""
var subtables: seq[tuple[key: string, value: TomlValueRef]] = @[]
for key, value in pairs(value):
block outer:
if value.kind == TomlValueKind.Table:
subtables.add((key: key, value: value))
elif value.kind == TomlValueKind.Array and
value.arrayVal.len > 0 and
value.arrayVal[0].kind == TomlValueKind.Table:
let tables = value.arrayVal.map(toTomlString)
for table in tables:
result = result & "[[" & key & "]]\n" & table & "\n"
else:
result = result & key.toKey & " = " & toTomlString(value) & "\n"
for kv in subtables:
let fullKey = (if parents.len > 0: parents & "." else: "") & kv.key.toKey
block outer:
for ikey, ivalue in pairs(kv.value.tableVal):
if ivalue.kind != TomlValueKind.Table:
result = result & "[" & fullKey & "]\n" &
kv.value.tableVal.toTomlString(fullKey) & "\n"
break outer
result = result & kv.value.tableVal.toTomlString(fullKey)
proc toTomlString*(value: TomlValueRef): string =
## Converts a TOML value to a TOML formatted string for output to a file.
case value.kind:
of TomlValueKind.Int: $value.intVal
of TomlValueKind.Float: $value.floatVal
of TomlValueKind.Bool: $value.boolVal
of TomlValueKind.Datetime: $value.dateTimeVal
of TomlValueKind.String: "\"" & value.stringVal & "\""
of TomlValueKind.Array:
if value.arrayVal.len == 0:
"[]"
elif value.arrayVal[0].kind == TomlValueKind.Table:
value.arrayVal.map(toTomlString).join("\n")
else:
"[" & value.arrayVal.map(toTomlString).join(", ") & "]"
of TomlValueKind.Table: value.tableVal.toTomlString
else:
"UNKNOWN"
proc newTString*(s: string): TomlValueRef =
## Creates a new `TomlValueKind.String TomlValueRef`.
TomlValueRef(kind: TomlValueKind.String, stringVal: s)
proc newTInt*(n: int64): TomlValueRef =
## Creates a new `TomlValueKind.Int TomlValueRef`.
TomlValueRef(kind: TomlValueKind.Int, intVal: n)
proc newTFloat*(n: float): TomlValueRef =
## Creates a new `TomlValueKind.Float TomlValueRef`.
TomlValueRef(kind: TomlValueKind.Float, floatVal: n)
proc newTBool*(b: bool): TomlValueRef =
## Creates a new `TomlValueKind.Bool TomlValueRef`.
TomlValueRef(kind: TomlValueKind.Bool, boolVal: b)
proc newTNull*(): TomlValueRef =
## Creates a new `JNull TomlValueRef`.
TomlValueRef(kind: TomlValueKind.None)
proc newTTable*(): TomlValueRef =
## Creates a new `TomlValueKind.Table TomlValueRef`
result = TomlValueRef(kind: TomlValueKind.Table)
new(result.tableVal)
result.tableVal[] = initOrderedTable[string, TomlValueRef](4)
proc newTArray*(): TomlValueRef =
## Creates a new `TomlValueKind.Array TomlValueRef`
TomlValueRef(kind: TomlValueKind.Array, arrayVal: @[])
proc getStr*(n: TomlValueRef, default: string = ""): string =
## Retrieves the string value of a `TomlValueKind.String TomlValueRef`.
##
## Returns ``default`` if ``n`` is not a ``TomlValueKind.String``, or if ``n`` is nil.
if n.isNil or n.kind != TomlValueKind.String: return default
else: return n.stringVal
proc getInt*(n: TomlValueRef, default: int = 0): int =
## Retrieves the int value of a `TomlValueKind.Int TomlValueRef`.
##
## Returns ``default`` if ``n`` is not a ``TomlValueKind.Int``, or if ``n`` is nil.
if n.isNil or n.kind != TomlValueKind.Int: return default
else: return int(n.intVal)
proc getBiggestInt*(n: TomlValueRef, default: int64 = 0): int64 =
## Retrieves the int64 value of a `TomlValueKind.Int TomlValueRef`.
##
## Returns ``default`` if ``n`` is not a ``TomlValueKind.Int``, or if ``n`` is nil.
if n.isNil or n.kind != TomlValueKind.Int: return default
else: return n.intVal
proc getFloat*(n: TomlValueRef, default: float = 0.0): float =
## Retrieves the float value of a `TomlValueKind.Float TomlValueRef`.
##
## Returns ``default`` if ``n`` is not a ``TomlValueKind.Float`` or ``TomlValueKind.Int``, or if ``n`` is nil.
if n.isNil: return default
case n.kind
of TomlValueKind.Float: return n.floatVal
of TomlValueKind.Int: return float(n.intVal)
else: return default
proc getBool*(n: TomlValueRef, default: bool = false): bool =
## Retrieves the bool value of a `TomlValueKind.Bool TomlValueRef`.
##
## Returns ``default`` if ``n`` is not a ``TomlValueKind.Bool``, or if ``n`` is nil.
if n.isNil or n.kind != TomlValueKind.Bool: return default
else: return n.boolVal
proc getTable*(n: TomlValueRef, default = new(TomlTableRef)): TomlTableRef =
## Retrieves the key, value pairs of a `TomlValueKind.Table TomlValueRef`.
##
## Returns ``default`` if ``n`` is not a ``TomlValueKind.Table``, or if ``n`` is nil.
if n.isNil or n.kind != TomlValueKind.Table: return default
else: return n.tableVal
proc getElems*(n: TomlValueRef, default: seq[TomlValueRef] = @[]): seq[TomlValueRef] =
## Retrieves the int value of a `TomlValueKind.Array TomlValueRef`.
##
## Returns ``default`` if ``n`` is not a ``TomlValueKind.Array``, or if ``n`` is nil.
if n.isNil or n.kind != TomlValueKind.Array: return default
else: return n.arrayVal
proc add*(father, child: TomlValueRef) =
## Adds `child` to a TomlValueKind.Array node `father`.
assert father.kind == TomlValueKind.Array
father.arrayVal.add(child)
proc add*(obj: TomlValueRef, key: string, val: TomlValueRef) =
## Sets a field from a `TomlValueKind.Table`.
assert obj.kind == TomlValueKind.Table
obj.tableVal[key] = val
proc `?`*(s: string): TomlValueRef =
## Generic constructor for TOML data. Creates a new `TomlValueKind.String TomlValueRef`.
TomlValueRef(kind: TomlValueKind.String, stringVal: s)
proc `?`*(n: int64): TomlValueRef =
## Generic constructor for TOML data. Creates a new `TomlValueKind.Int TomlValueRef`.
TomlValueRef(kind: TomlValueKind.Int, intVal: n)
proc `?`*(n: float): TomlValueRef =
## Generic constructor for TOML data. Creates a new `TomlValueKind.Float TomlValueRef`.
TomlValueRef(kind: TomlValueKind.Float, floatVal: n)
proc `?`*(b: bool): TomlValueRef =
## Generic constructor for TOML data. Creates a new `TomlValueKind.Bool TomlValueRef`.
TomlValueRef(kind: TomlValueKind.Bool, boolVal: b)
proc `?`*(keyVals: openArray[tuple[key: string, val: TomlValueRef]]): TomlValueRef =
## Generic constructor for TOML data. Creates a new `TomlValueKind.Table TomlValueRef`
if keyVals.len == 0: return newTArray()
result = newTTable()
for key, val in items(keyVals): result.tableVal[key] = val
template `?`*(j: TomlValueRef): TomlValueRef = j
proc `?`*[T](elements: openArray[T]): TomlValueRef =
## Generic constructor for TOML data. Creates a new `TomlValueKind.Array TomlValueRef`
result = newTArray()
for elem in elements: result.add(?elem)
when false:
# For 'consistency' we could do this, but that only pushes people further
# into that evil comfort zone where they can use Nim without understanding it
# causing problems later on.
proc `?`*(elements: set[bool]): TomlValueRef =
## Generic constructor for TOML data. Creates a new `TomlValueKind.Table TomlValueRef`.
## This can only be used with the empty set ``{}`` and is supported
## to prevent the gotcha ``%*{}`` which used to produce an empty
## TOML array.
result = newTTable()
assert false notin elements, "usage error: only empty sets allowed"
assert true notin elements, "usage error: only empty sets allowed"
proc `?`*(o: object): TomlValueRef =
## Generic constructor for TOML data. Creates a new `TomlValueKind.Table TomlValueRef`
result = newTTable()
for k, v in o.fieldPairs: result[k] = ?v
proc `?`*(o: ref object): TomlValueRef =
## Generic constructor for TOML data. Creates a new `TomlValueKind.Table TomlValueRef`
if o.isNil:
result = newTNull()
else:
result = ?(o[])
proc `?`*(o: enum): TomlValueRef =
## Construct a TomlValueRef that represents the specified enum value as a
## string. Creates a new ``TomlValueKind.String TomlValueRef``.
result = ?($o)
import macros
proc toToml(x: NimNode): NimNode {.compileTime.} =
case x.kind
of nnkBracket: # array
if x.len == 0: return newCall(bindSym"newTArray")
result = newNimNode(nnkBracket)
for i in 0 ..< x.len:
result.add(toToml(x[i]))
result = newCall(bindSym("?", brOpen), result)
of nnkTableConstr: # object
if x.len == 0: return newCall(bindSym"newTTable")
result = newNimNode(nnkTableConstr)
for i in 0 ..< x.len:
x[i].expectKind nnkExprColonExpr
result.add newTree(nnkExprColonExpr, x[i][0], toToml(x[i][1]))
result = newCall(bindSym("?", brOpen), result)
of nnkCurly: # empty object
x.expectLen(0)
result = newCall(bindSym"newTTable")
of nnkNilLit:
result = newCall(bindSym"newTNull")
else:
result = newCall(bindSym("?", brOpen), x)
macro `?*`*(x: untyped): untyped =
## Convert an expression to a TomlValueRef directly, without having to specify
## `?` for every element.
result = toToml(x)
echo result.repr
proc toTomlValue(x: NimNode): NimNode {.compileTime.} =
newCall(bindSym("?", brOpen), x)
proc toTomlNew(x: NimNode): NimNode {.compileTime.} =
echo x.treeRepr
var
i = 0
curTable: NimNode = nil
while i < x.len:
echo x[i].kind
case x[i].kind:
of nnkAsgn:
if curTable.isNil:
curTable = newNimNode(nnkTableConstr)
result = curTable
curTable.add newTree(nnkExprColonExpr, newLit($x[i][0]), toTomlValue(x[i][1]))
of nnkBracket:
if curTable.isNil:
curTable = newNimNode(nnkTableConstr)
result = curTable
else:
var table = newNimNode(nnkTableConstr)
result.add newTree(nnkExprColonExpr, newLit($x[i][0]), newCall(bindSym("?", brOpen), table))
curTable = table
else: discard
i += 1
result = newCall(bindSym("?", brOpen), result)
macro `parseToml`*(x: untyped): untyped =
## Convert an expression to a TomlValueRef directly, without having to specify
## `?` for every element.
result = toTomlNew(x)
echo result.repr
func `==`* (a, b: TomlValueRef): bool =
## Check two nodes for equality
if a.isNil:
if b.isNil: return true
return false
elif b.isNil or a.kind != b.kind:
return false
else:
case a.kind
of TomlValueKind.String:
result = a.stringVal == b.stringVal
of TomlValueKind.Int:
result = a.intVal == b.intVal
of TomlValueKind.Float:
result = a.floatVal == b.floatVal
of TomlValueKind.Bool:
result = a.boolVal == b.boolVal
of TomlValueKind.None:
result = true
of TomlValueKind.Array:
result = a.arrayVal == b.arrayVal
of TomlValueKind.Table:
# we cannot use OrderedTable's equality here as
# the order does not matter for equality here.
if a.tableVal.len != b.tableVal.len: return false
for key, val in a.tableVal:
if not b.tableVal.hasKey(key): return false
{.noSideEffect.}:
if b.tableVal[key] != val: return false
result = true
of TomlValueKind.DateTime:
result =
a.dateTimeVal.date.year == b.dateTimeVal.date.year and
a.dateTimeVal.date.month == b.dateTimeVal.date.month and
a.dateTimeVal.date.day == b.dateTimeVal.date.day and
a.dateTimeVal.time.hour == b.dateTimeVal.time.hour and
a.dateTimeVal.time.minute == b.dateTimeVal.time.minute and
a.dateTimeVal.time.second == b.dateTimeVal.time.second and
a.dateTimeVal.time.subsecond == b.dateTimeVal.time.subsecond and
a.dateTimeVal.shift == b.dateTimeVal.shift and
(a.dateTimeVal.shift == true and
(a.dateTimeVal.isShiftPositive == b.dateTimeVal.isShiftPositive and
a.dateTimeVal.zoneHourShift == b.dateTimeVal.zoneHourShift and
a.dateTimeVal.zoneMinuteShift == b.dateTimeVal.zoneMinuteShift)) or
a.dateTimeVal.shift == false
of TomlValueKind.Date:
result =
a.dateVal.year == b.dateVal.year and
a.dateVal.month == b.dateVal.month and
a.dateVal.day == b.dateVal.day
of TomlValueKind.Time:
result =
a.timeVal.hour == b.timeVal.hour and
a.timeVal.minute == b.timeVal.minute and
a.timeVal.second == b.timeVal.second and
a.timeVal.subsecond == b.timeVal.subsecond
import hashes
proc hash*(n: OrderedTable[string, TomlValueRef]): Hash {.noSideEffect.}
proc hash*(n: TomlValueRef): Hash {.noSideEffect.} =
## Compute the hash for a TOML node
case n.kind
of TomlValueKind.Array:
result = hash(n.arrayVal)
of TomlValueKind.Table:
result = hash(n.tableVal[])
of TomlValueKind.Int:
result = hash(n.intVal)
of TomlValueKind.Float:
result = hash(n.floatVal)
of TomlValueKind.Bool:
result = hash(n.boolVal.int)
of TomlValueKind.String:
result = hash(n.stringVal)
of TomlValueKind.None:
result = Hash(0)
of TomlValueKind.DateTime:
result = hash($n.dateTimeVal)
of TomlValueKind.Date:
result = hash($n.dateVal)
of TomlValueKind.Time:
result = hash($n.timeVal)
proc hash*(n: OrderedTable[string, TomlValueRef]): Hash =
for key, val in n:
result = result xor (hash(key) !& hash(val))
result = !$result
proc len*(n: TomlValueRef): int =
## If `n` is a `TomlValueKind.Array`, it returns the number of elements.
## If `n` is a `TomlValueKind.Table`, it returns the number of pairs.
## Else it returns 0.
case n.kind
of TomlValueKind.Array: result = n.arrayVal.len
of TomlValueKind.Table: result = n.tableVal.len
else: discard
proc `[]`*(node: TomlValueRef, name: string): TomlValueRef {.inline.} =
## Gets a field from a `TomlValueKind.Table`, which must not be nil.
## If the value at `name` does not exist, raises KeyError.
assert(not isNil(node))
assert(node.kind == TomlValueKind.Table)
result = node.tableVal[name]
proc `[]`*(node: TomlValueRef, index: int): TomlValueRef {.inline.} =
## Gets the node at `index` in an Array. Result is undefined if `index`
## is out of bounds, but as long as array bound checks are enabled it will
## result in an exception.
assert(not isNil(node))
assert(node.kind == TomlValueKind.Array)
return node.arrayVal[index]
proc hasKey*(node: TomlValueRef, key: string): bool =
## Checks if `key` exists in `node`.
assert(node.kind == TomlValueKind.Table)
result = node.tableVal.hasKey(key)
proc contains*(node: TomlValueRef, key: string): bool =
## Checks if `key` exists in `node`.
assert(node.kind == TomlValueKind.Table)
node.tableVal.hasKey(key)
proc contains*(node: TomlValueRef, val: TomlValueRef): bool =
## Checks if `val` exists in array `node`.
assert(node.kind == TomlValueKind.Array)
find(node.arrayVal, val) >= 0
proc existsKey*(node: TomlValueRef, key: string): bool {.deprecated.} = node.hasKey(key)
## Deprecated for `hasKey`
proc `[]=`*(obj: TomlValueRef, key: string, val: TomlValueRef) {.inline.} =
## Sets a field from a `TomlValueKind.Table`.
assert(obj.kind == TomlValueKind.Table)
obj.tableVal[key] = val
proc `{}`*(node: TomlValueRef, keys: varargs[string]): TomlValueRef =
## Traverses the node and gets the given value. If any of the
## keys do not exist, returns ``nil``. Also returns ``nil`` if one of the
## intermediate data structures is not an object.
result = node
for key in keys:
if isNil(result) or result.kind != TomlValueKind.Table:
return nil
result = result.tableVal.getOrDefault(key)
proc getOrDefault*(node: TomlValueRef, key: string): TomlValueRef =
## Gets a field from a `node`. If `node` is nil or not an object or
## value at `key` does not exist, returns nil
if not isNil(node) and node.kind == TomlValueKind.Table:
result = node.tableVal.getOrDefault(key)
template simpleGetOrDefault*{`{}`(node, [key])}(node: TomlValueRef, key: string): TomlValueRef = node.getOrDefault(key)
proc `{}=`*(node: TomlValueRef, keys: varargs[string], value: TomlValueRef) =
## Traverses the node and tries to set the value at the given location
## to ``value``. If any of the keys are missing, they are added.
var node = node
for i in 0..(keys.len-2):
if not node.hasKey(keys[i]):
node[keys[i]] = newTTable()
node = node[keys[i]]
node[keys[keys.len-1]] = value
proc delete*(obj: TomlValueRef, key: string) =
## Deletes ``obj[key]``.
assert(obj.kind == TomlValueKind.Table)
if not obj.tableVal.hasKey(key):
raise newException(IndexDefect, "key not in object")
obj.tableVal.del(key)
proc copy*(p: TomlValueRef): TomlValueRef =
## Performs a deep copy of `a`.
case p.kind
of TomlValueKind.String:
result = newTString(p.stringVal)
of TomlValueKind.Int:
result = newTInt(p.intVal)
of TomlValueKind.Float:
result = newTFloat(p.floatVal)
of TomlValueKind.Bool:
result = newTBool(p.boolVal)
of TomlValueKind.None:
result = newTNull()
of TomlValueKind.Table:
result = newTTable()
for key, val in pairs(p.tableVal):
result.tableVal[key] = copy(val)
of TomlValueKind.Array:
result = newTArray()
for i in items(p.arrayVal):
result.arrayVal.add(copy(i))
of TomlValueKind.DateTime:
new(result)
result[] = p[]
of TomlValueKind.Date:
new(result)
result[] = p[]
of TomlValueKind.Time:
new(result)
result[] = p[]