packages feed

dojang-0.2.0: src/Dojang/Syntax/EnvironmentPredicate/Writer.hs

{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}

module Dojang.Syntax.EnvironmentPredicate.Writer
  ( writeEnvironmentPredicate
  )
where

import Dojang.Types.Environment (Architecture, OperatingSystem)
import Dojang.Types.EnvironmentPredicate (EnvironmentPredicate (..))
import Dojang.Types.MonikerName (MonikerName)

import Data.CaseInsensitive (CI (original))
import Data.Char (isAlpha, isAlphaNum, isControl)
import Data.List.NonEmpty (toList)
import Data.Text
  ( Text
  , all
  , concatMap
  , elem
  , intercalate
  , justifyRight
  , pack
  , singleton
  , uncons
  )
import Numeric (showHex)
import Prelude hiding (all, concatMap, elem, reverse)


-- | Turns an 'EnvironmentPredicate' into a string that can be used in a
-- @dojang.toml@ file.  The string can be parsed back into an
-- 'EnvironmentPredicate' using
-- 'Dojang.Syntax.EnvironmentPredicate.Parser.parseEnvironmentPredicate'.
writeEnvironmentPredicate :: EnvironmentPredicate -> Text
writeEnvironmentPredicate = fst . write


write
  :: EnvironmentPredicate
  -> (Text, Bool)
-- \^ @(when, can be split by &&)@
write Always = ("always", False)
write (OperatingSystem os) =
  ("os = " <> stringLiteral (original os.identifier), False)
write (Architecture arch) =
  ("arch = " <> stringLiteral (original arch.identifier), False)
write (KernelName kernel) =
  ("kernel = " <> stringLiteral (original kernel), False)
write (KernelRelease rel) =
  ("kernel-release = " <> stringLiteral (original rel), False)
write (KernelReleasePrefix prefix) =
  ("kernel-release ^= " <> stringLiteral (original prefix), False)
write (KernelReleaseSuffix suffix) =
  ("kernel-release $= " <> stringLiteral (original suffix), False)
write (Moniker moniker) =
  ("moniker = " <> stringLiteral (original moniker.name), False)
write (And predicates) =
  case (excludeOses, excludeArchitectures, excludeMonikers) of
    (_ : _, [], [])
      | length excludeOses == length predicates ->
          ( "os not in "
              <> strings [original os.identifier | os <- excludeOses]
          , False
          )
    ([], _ : _, [])
      | length excludeArchitectures == length predicates ->
          ( "arch not in "
              <> strings
                [ original arch.identifier
                | arch <- excludeArchitectures
                ]
          , False
          )
    ([], [], _ : _)
      | length excludeMonikers == length predicates ->
          ( "moniker not in "
              <> strings [original moniker.name | moniker <- excludeMonikers]
          , False
          )
    _ ->
      ( intercalate
          " && "
          [ let (when', needParentheses) = write p
            in if needParentheses then "(" <> when' <> ")" else when'
          | p <- toList predicates
          ]
      , False
      )
 where
  excludeOses :: [OperatingSystem]
  excludeOses = [os | Not (OperatingSystem os) <- toList predicates]
  excludeArchitectures :: [Architecture]
  excludeArchitectures = [arch | Not (Architecture arch) <- toList predicates]
  excludeMonikers :: [MonikerName]
  excludeMonikers = [moniker | Not (Moniker moniker) <- toList predicates]
write (Or predicates) =
  case (oses, architectures, monikers) of
    (_ : _, [], [])
      | length oses == length predicates ->
          ("os in " <> strings [original os.identifier | os <- oses], False)
    ([], _ : _, [])
      | length architectures == length predicates ->
          ( "arch in "
              <> strings [original arch.identifier | arch <- architectures]
          , False
          )
    ([], [], _ : _)
      | length monikers == length predicates ->
          ( "moniker in "
              <> strings [original moniker.name | moniker <- monikers]
          , False
          )
    _ -> (intercalate " || " (fst . write <$> toList predicates), True)
 where
  oses :: [OperatingSystem]
  oses = [os | OperatingSystem os <- toList predicates]
  architectures :: [Architecture]
  architectures = [arch | Architecture arch <- toList predicates]
  monikers :: [MonikerName]
  monikers = [moniker | Moniker moniker <- toList predicates]
write (Not Always) = ("never", False)
write (Not (OperatingSystem os)) =
  ("os != " <> stringLiteral (original os.identifier), False)
write (Not (Architecture arch)) =
  ("arch != " <> stringLiteral (original arch.identifier), False)
write (Not (Moniker moniker)) =
  ("moniker != " <> stringLiteral (original moniker.name), False)
write (Not p) = ("!(" <> fst (write p) <> ")", False)


stringLiteral :: Text -> Text
stringLiteral v = case uncons v of
  Nothing -> "\"\""
  Just (c, cs) | isAlpha c && all isAlphaNum cs -> v
  _ | '"' `elem` v && not ('\'' `elem` v) -> "'" <> escapeText False v <> "'"
  _ -> "\"" <> escapeText True v <> "\""
 where
  escapeText :: Bool -> Text -> Text
  escapeText doubleQuote = concatMap (escape doubleQuote)
  escape :: Bool -> Char -> Text
  escape True '"' = "\\\""
  escape False '\'' = "\\'"
  escape _ '\\' = "\\\\"
  escape _ '\b' = "\\b"
  escape _ '\f' = "\\f"
  escape _ '\n' = "\\n"
  escape _ '\r' = "\\r"
  escape _ '\t' = "\\t"
  escape _ '\v' = "\\v"
  escape _ '\0' = "\\0"
  escape _ c
    | not (isControl c) && c < '\x80' = singleton c
    | c <= '\xff' = "\\x" <> justifyRight 2 '0' (hex c)
    | c <= '\xffff' = "\\u" <> justifyRight 4 '0' (hex c) -- cSpell: disable-line
    | otherwise = "\\U" <> justifyRight 8 '0' (hex c)
   where
    hex :: Char -> Text
    hex = pack . (`showHex` "") . fromEnum


strings :: [Text] -> Text
strings = ("(" <>) . (<> ")") . intercalate ", " . fmap stringLiteral