{-|
Module      : Trivialini
Description : Ultra light weight ini file parser
Copyright   : (c) 2021-2024 Mirko Westermeier
License     : MIT

Ultra light weight ini file parser
-}

module Trivialini
  (
  -- * Ini files and data
  -- $intro
    readIniFile
  , readIniFileStrings
  -- * Ini data is a Map of Maps
  , Ini(..)
  -- * Simple 'String' 'Map' conversion
  , toStringMap
  ) where

import Trivialini.SafeTypes
import Data.Map (Map, fromList, assocs, mapKeys)
import Data.List
import Data.Maybe
import Text.ParserCombinators.ReadP
import Control.Monad

{- $intro
Consider a simple ini file @config.ini@ like this:

@
[something]
foo = bar

[something else]
answer = 42
name = Boaty McBoatface
@

There are two /sections/ (inbetween @"["@ and @"]"@) defined, @"something"@
and @"something else"@. These sections contain a dictionary of strings each,
the keys being some string followed by @"="@, and anything else until end of
the line as values. The leading and trailing spaces in section headers, keys
and values are trimmed.
-}

-- | Read t'Ini' data from a given filename
readIniFile :: FilePath -> IO Ini
readIniFile :: String -> IO Ini
readIniFile = (String -> Ini) -> IO String -> IO Ini
forall a b. (a -> b) -> IO a -> IO b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap String -> Ini
forall a. Read a => String -> a
read (IO String -> IO Ini) -> (String -> IO String) -> String -> IO Ini
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> IO String
readFile

-- | Like 'readIniFile', but results in a stringified nested map
readIniFileStrings :: FilePath -> IO (Map String (Map String String))
readIniFileStrings :: String -> IO (Map String (Map String String))
readIniFileStrings = (String -> Map String (Map String String))
-> IO String -> IO (Map String (Map String String))
forall a b. (a -> b) -> IO a -> IO b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (Ini -> Map String (Map String String)
toStringMap (Ini -> Map String (Map String String))
-> (String -> Ini) -> String -> Map String (Map String String)
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> Ini
forall a. Read a => String -> a
read) (IO String -> IO (Map String (Map String String)))
-> (String -> IO String)
-> String
-> IO (Map String (Map String String))
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> IO String
readFile

-- | As ini files consist of sections with a name, each with a list of
-- key-value pairs, A "two-dimensional" 'Map' of 'String's seems to be very
-- natural. However, since the formatting of ini files doesn't allow arbitrary
-- arbitrary characters, restricted types are used here, that are thin wrappers
-- around 'String's:
newtype Ini = Ini { Ini -> Map IniHeading (Map IniKey IniValue)
sections :: Map IniHeading (Map IniKey IniValue) }
  deriving
    ( Ini -> Ini -> Bool
(Ini -> Ini -> Bool) -> (Ini -> Ini -> Bool) -> Eq Ini
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: Ini -> Ini -> Bool
== :: Ini -> Ini -> Bool
$c/= :: Ini -> Ini -> Bool
/= :: Ini -> Ini -> Bool
Eq -- ^ Default Eq instance
    )

-- | Stringification of t'Ini' data. The result can be parsed again as t'Ini'
-- data.
instance Show Ini where
  show :: Ini -> String
show = [String] -> String
unlines ([String] -> String) -> (Ini -> [String]) -> Ini -> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ((IniHeading, Map IniKey IniValue) -> String)
-> [(IniHeading, Map IniKey IniValue)] -> [String]
forall a b. (a -> b) -> [a] -> [b]
map (IniHeading, Map IniKey IniValue) -> String
section ([(IniHeading, Map IniKey IniValue)] -> [String])
-> (Ini -> [(IniHeading, Map IniKey IniValue)]) -> Ini -> [String]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Map IniHeading (Map IniKey IniValue)
-> [(IniHeading, Map IniKey IniValue)]
forall k a. Map k a -> [(k, a)]
assocs (Map IniHeading (Map IniKey IniValue)
 -> [(IniHeading, Map IniKey IniValue)])
-> (Ini -> Map IniHeading (Map IniKey IniValue))
-> Ini
-> [(IniHeading, Map IniKey IniValue)]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Ini -> Map IniHeading (Map IniKey IniValue)
sections
    where section :: (IniHeading, Map IniKey IniValue) -> String
section (IniHeading
name, Map IniKey IniValue
sec) = String
"[" String -> ShowS
forall a. [a] -> [a] -> [a]
++ IniHeading -> String
getHeading IniHeading
name String -> ShowS
forall a. [a] -> [a] -> [a]
++ String
"]\n" String -> ShowS
forall a. [a] -> [a] -> [a]
++ Map IniKey IniValue -> String
pairs Map IniKey IniValue
sec
          pairs :: Map IniKey IniValue -> String
pairs               = [String] -> String
unlines ([String] -> String)
-> (Map IniKey IniValue -> [String])
-> Map IniKey IniValue
-> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ((IniKey, IniValue) -> String) -> [(IniKey, IniValue)] -> [String]
forall a b. (a -> b) -> [a] -> [b]
map (IniKey, IniValue) -> String
pair ([(IniKey, IniValue)] -> [String])
-> (Map IniKey IniValue -> [(IniKey, IniValue)])
-> Map IniKey IniValue
-> [String]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Map IniKey IniValue -> [(IniKey, IniValue)]
forall k a. Map k a -> [(k, a)]
assocs
          pair :: (IniKey, IniValue) -> String
pair (IniKey
k, IniValue
v)         = IniKey -> String
getKey IniKey
k String -> ShowS
forall a. [a] -> [a] -> [a]
++ String
" = " String -> ShowS
forall a. [a] -> [a] -> [a]
++ IniValue -> String
getValue IniValue
v

-- | Parsing of Ini strings.
instance Read Ini where
  readsPrec :: Int -> ReadS Ini
readsPrec Int
_ = ReadP Ini -> ReadS Ini
forall a. ReadP a -> ReadS a
readP_to_S ReadP Ini
parser
    where parser :: ReadP Ini
parser  = Map IniHeading (Map IniKey IniValue) -> Ini
Ini (Map IniHeading (Map IniKey IniValue) -> Ini)
-> ([(IniHeading, Map IniKey IniValue)]
    -> Map IniHeading (Map IniKey IniValue))
-> [(IniHeading, Map IniKey IniValue)]
-> Ini
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [(IniHeading, Map IniKey IniValue)]
-> Map IniHeading (Map IniKey IniValue)
forall k a. Ord k => [(k, a)] -> Map k a
fromList ([(IniHeading, Map IniKey IniValue)] -> Ini)
-> ReadP [(IniHeading, Map IniKey IniValue)] -> ReadP Ini
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> ReadP (IniHeading, Map IniKey IniValue)
-> ReadP [(IniHeading, Map IniKey IniValue)]
forall a. ReadP a -> ReadP [a]
many ReadP (IniHeading, Map IniKey IniValue)
section
          section :: ReadP (IniHeading, Map IniKey IniValue)
section = do  String
name <- ShowS
trim ShowS -> ReadP String -> ReadP String
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> ReadP Char -> ReadP String -> ReadP String -> ReadP String
forall open close a.
ReadP open -> ReadP close -> ReadP a -> ReadP a
between (Char -> ReadP Char
char Char
'[') (Char -> ReadP Char
char Char
']' ReadP Char -> ReadP String -> ReadP String
forall a b. ReadP a -> ReadP b -> ReadP b
forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> ReadP String
nls) (String -> ReadP String
no String
"=\n]")
                        Bool -> ReadP ()
forall (f :: * -> *). Alternative f => Bool -> f ()
guard (Bool -> ReadP ()) -> Bool -> ReadP ()
forall a b. (a -> b) -> a -> b
$ String -> Bool
isValidHeading String
name
                        [(IniKey, IniValue)]
pairs <- ReadP (IniKey, IniValue) -> ReadP [(IniKey, IniValue)]
forall a. ReadP a -> ReadP [a]
many ReadP (IniKey, IniValue)
pair
                        (IniHeading, Map IniKey IniValue)
-> ReadP (IniHeading, Map IniKey IniValue)
forall a. a -> ReadP a
forall (m :: * -> *) a. Monad m => a -> m a
return (Maybe IniHeading -> IniHeading
forall a. HasCallStack => Maybe a -> a
fromJust (String -> Maybe IniHeading
mkHdg String
name), [(IniKey, IniValue)] -> Map IniKey IniValue
forall k a. Ord k => [(k, a)] -> Map k a
fromList [(IniKey, IniValue)]
pairs)
          pair :: ReadP (IniKey, IniValue)
pair    = do  String
key <- ShowS
trim ShowS -> ReadP String -> ReadP String
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> String -> ReadP String
no String
"\n[="
                        String
val <- ShowS
trim ShowS -> ReadP String -> ReadP String
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> ReadP Char -> ReadP String -> ReadP String -> ReadP String
forall open close a.
ReadP open -> ReadP close -> ReadP a -> ReadP a
between (Char -> ReadP Char
char Char
'=') ReadP String
nls (String -> ReadP String
no String
"\n")
                        Bool -> ReadP ()
forall (f :: * -> *). Alternative f => Bool -> f ()
guard (Bool -> ReadP ()) -> Bool -> ReadP ()
forall a b. (a -> b) -> a -> b
$ String -> Bool
isValidKey String
key Bool -> Bool -> Bool
&& String -> Bool
isValidValue String
val
                        (IniKey, IniValue) -> ReadP (IniKey, IniValue)
forall a. a -> ReadP a
forall (m :: * -> *) a. Monad m => a -> m a
return (Maybe IniKey -> IniKey
forall a. HasCallStack => Maybe a -> a
fromJust (String -> Maybe IniKey
mkKey String
key), Maybe IniValue -> IniValue
forall a. HasCallStack => Maybe a -> a
fromJust (String -> Maybe IniValue
mkVal String
val))
          nls :: ReadP String
nls     = (Char -> Bool) -> ReadP String
munch1 (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
==Char
'\n')
          no :: String -> ReadP String
no      = (Char -> Bool) -> ReadP String
munch1 ((Char -> Bool) -> ReadP String)
-> (String -> Char -> Bool) -> String -> ReadP String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Char -> String -> Bool) -> String -> Char -> Bool
forall a b c. (a -> b -> c) -> b -> a -> c
flip Char -> String -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
notElem
          trim :: ShowS
trim    = (Char -> Bool) -> ShowS
forall a. (a -> Bool) -> [a] -> [a]
dropWhile (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
==Char
' ') ShowS -> ShowS -> ShowS
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Char -> Bool) -> ShowS
forall a. (a -> Bool) -> [a] -> [a]
dropWhileEnd (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
==Char
' ')

-- | Convert ini data (with restricted types) to nested 'String' 'Map's.
toStringMap :: Ini -> Map String (Map String String)
toStringMap :: Ini -> Map String (Map String String)
toStringMap = (IniHeading -> String)
-> Map IniHeading (Map String String)
-> Map String (Map String String)
forall k2 k1 a. Ord k2 => (k1 -> k2) -> Map k1 a -> Map k2 a
mapKeys IniHeading -> String
getHeading (Map IniHeading (Map String String)
 -> Map String (Map String String))
-> (Ini -> Map IniHeading (Map String String))
-> Ini
-> Map String (Map String String)
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Map IniKey IniValue -> Map String String)
-> Map IniHeading (Map IniKey IniValue)
-> Map IniHeading (Map String String)
forall a b. (a -> b) -> Map IniHeading a -> Map IniHeading b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap Map IniKey IniValue -> Map String String
sectionToStringMap (Map IniHeading (Map IniKey IniValue)
 -> Map IniHeading (Map String String))
-> (Ini -> Map IniHeading (Map IniKey IniValue))
-> Ini
-> Map IniHeading (Map String String)
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Ini -> Map IniHeading (Map IniKey IniValue)
sections
  where sectionToStringMap :: Map IniKey IniValue -> Map String String
sectionToStringMap = (IniKey -> String) -> Map IniKey String -> Map String String
forall k2 k1 a. Ord k2 => (k1 -> k2) -> Map k1 a -> Map k2 a
mapKeys IniKey -> String
getKey (Map IniKey String -> Map String String)
-> (Map IniKey IniValue -> Map IniKey String)
-> Map IniKey IniValue
-> Map String String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (IniValue -> String) -> Map IniKey IniValue -> Map IniKey String
forall a b. (a -> b) -> Map IniKey a -> Map IniKey b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap IniValue -> String
getValue