Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

i wanted to practice parsec and thought about doing a TOML parser for fun.

i am looking for both directions on how to fix the remaining problems, but even more some hints on my style.

the one problem is, that it won't parse top-level assignments (not within a section). i just couldn't think about a good data structure to capture that. but that's not a fundamental problem.

the real problem is that tArray cannot reliably parse an array of floats. tInt and tFloat overlap, in that both can (and should) parse 1 (as integer 1 or float 1.0). sepBy and choices do not really work together for me it seems. i mitigated the problem somewhat by using sepBy1 which will only fail for e.g. [1, 1.2], but won't allow empty arrays. a failed attempt to greedily parse the whole array is commented shown below.

thank you in advance!

-- a parser based on parsec to parse toml files
module Toml (tDocument) where

import Text.ParserCombnators.Parsec hiding (many, (<|>))
import Control.Applicative
import Numeric (readDec, readFloat, readSigned)
import Data.Time.Format (readsTime)
import Data.Time.Clock (UTCTime)
import System.Locale (defaultTimeLocale)


data TValue = TString String
            | TInteger Int
            | TFloat Float
            | TBool Bool
            | TDateTime UTCTime
            | TArray [TValue]
    deriving (Show)

-- get a parser from a read function
parserFromReads :: ReadS a -> Parser a
parserFromReads readFun = do s <- getInput
                             case readFun s of
                                  [(n, s')] -> n <$ setInput s'
                                  _ -> empty

spaces' :: Parser ()
spaces' = skipMany (oneOf "\t ")

trim :: Parser a -> Parser a
trim parser = spaces *> parser <* spaces

trim' :: Parser a -> Parser a
trim' parser = spaces' *> parser <* spaces'

eol' :: Parser ()
eol' = spaces' <* skipMany tComment <* eol <* skipCommentAndNewlines
    where eol = char '\n'

skipCommentAndNewlines :: Parser ()
skipCommentAndNewlines = spaces >> skipMany (tComment >> spaces)


-- primitives
-------------

tString :: Parser String
-- tString = between (char '"') (char '"') (many tChar)
tString = char '"' *> many tChar <* char '"'
    where tChar :: Parser Char
          tChar = char '\\' *> tEscape <|> satisfy (`notElem` "\\\"")
          tEscape :: Parser Char
          tEscape = choice $ zipWith decode "0tnr\"\\" "\0\t\n\r\"\\"
              where decode c r = r <$ char c

tNum :: Real a => ReadS a -> Parser a
tNum readPrim = parserFromReads $ readSigned readPrim

tInt :: Parser Int
tInt = tNum readDec

tFloat :: Parser Float
tFloat = tNum readFloat

tBool :: Parser Bool
tBool = True <$ string "true"
    <|> False <$ string "false"

tDatetime :: Parser UTCTime
tDatetime = parserFromReads $ readsTime defaultTimeLocale "%FT%XZ"

-- note: trim accepts newline, so this is ok
tArray :: Parser [TValue]
tArray = char '[' *> spaces' *> choice parsers <* char ']'
    where prepParser :: Parser TValue -> Parser [TValue]
          prepParser parser = trim parser `sepBy1` char ','
          parsers = map prepParser constructor_parsers

--tArray = choice parsers
-- where outer :: Parser TValue -> Parser [TValue]
-- outer val = char '[' *> spaces' *> inner val <* spaces' <* char ']'
-- inner :: Parser TValue -> Parser [TValue]
-- inner val = trim val `sepBy` char ','
-- parsers = map outer constructor_parsers


tValue :: Parser TValue
tValue = choice constructor_parsers <?> "TOML value"

constructor_parsers :: [ Parser TValue]
constructor_parsers = [ TString <$> tString
                , TDateTime <$> tDatetime
                , TInteger <$> tInt
                , TFloat <$> tFloat
                , TBool <$> tBool
                , TArray <$> tArray
                ]


-- ignore comments
tComment :: Parser ()
-- tComment = skipMany1 (char '#') >> skipMany (noneOf "\n")
tComment = char '#' *> skipMany (noneOf "\n")

tIdentifier :: Parser String
tIdentifier = many1 (alphaNum <|> oneOf "_")

tAssignment :: Parser (String, TValue)
tAssignment = (,) <$> (tIdentifier <* trim' (char '=')) <*> tValue <* eol'

tKeygroupHeader :: Parser [String]
tKeygroupHeader = char '[' *> trim' (tIdentifier `sepBy1` char '.') <* char ']' <* eol'

tKeygroup :: Parser ([String], [(String, TValue)])
tKeygroup = (,) <$> tKeygroupHeader
                <*> many tAssignment

tDocument :: Parser [([String], [(String, TValue)])]
tDocument = skipCommentAndNewlines *> many tKeygroup
share|improve this question

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Browse other questions tagged or ask your own question.