content: {-# LANGUAGE DeriveLift #-}
  {-# LANGUAGE OverloadedStrings #-}
  {-# LANGUAGE ScopedTypeVariables #-}
  {-# LANGUAGE TupleSections #-}
  
  module DataCSV where
  
  import Data.Bifunctor (first)
  import qualified Data.Foldable as F
  import qualified Data.Text as T
  import qualified Data.Text.IO as T
  import Language.Haskell.TH.Syntax (Lift (..))
  import qualified Toml
  import Validation
  
  data CsvExample = CsvExample
      { name :: String
      , csv :: String
      , generated :: String
      }
      deriving (Lift, Show)
  
  keyText :: Toml.Key -> T.Text
  keyText = F.fold . cleanup . map Toml.unPiece . F.toList . Toml.unKey
    where
      -- Top-level table names are parsed as @"name" :|
      -- ["name"]@. Remove that duplication here.
      cleanup [x, y] | x == y = [x]
      cleanup x = x
  
  -- | Parse a TOML file that is a top-level table whose values are all
  -- the same type. The @tomland@ codec API is centered around starting
  -- with a key, but a top-level table does not have a key, so we must
  -- use the lower level 'Toml.parse' and 'Toml.tomlTables' before
  -- repeatedly applying the provided 'Toml.TomlCodec'.
  parseFileOf :: forall a. Toml.TomlCodec a -> T.Text -> Either [T.Text] [(T.Text, a)]
  parseFileOf codec =
      first (map Toml.prettyTomlDecodeError)
          . validationToEither
          . traverse (uncurry go)
          . Toml.toList
          . Toml.tomlTables
          . either (error . show) id
          . Toml.parse
    where
      go :: Toml.Key -> Toml.TOML -> Validation [Toml.TomlDecodeError] (T.Text, a)
      go k v = (keyText k,) <$> Toml.runTomlCodec codec v
  
  parseExamples :: FilePath -> IO (Either [T.Text] [CsvExample])
  parseExamples = fmap (fmap (map mkExample) . parseFileOf exampleCodec) . T.readFile
    where
      exampleCodec = Toml.pair (Toml.string "csv") (Toml.string "generated")
      mkExample (name', (csv', generated')) =
          CsvExample (T.unpack name') csv' generated'
  
  -- | Wraps 'parseExamples' to call 'error' on any parse errors.
  examplesFrom :: FilePath -> IO [CsvExample]
  examplesFrom = fmap (either (error . show) id) . parseExamples
  