packages feed

clash-lib-1.10.0: tools/v16-upgrade-primitives.hs

{- Utility executable to convert "old-style" JSON primitives to "new-style"
   YAML ones. See https://github.com/clash-lang/clash-compiler/pull/2009.
-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}

module Main where

import qualified Data.Aeson.KeyMap as Aeson
import Data.String (IsString)
import qualified Data.Text.Lazy as LazyText
import qualified Data.Text.Lazy.Encoding as LazyText

import qualified Data.Aeson.Extra as AesonExtra
import qualified Data.Aeson as Aeson
import qualified Data.Yaml as Yaml
import qualified Data.ByteString.Lazy as ByteString
import qualified Data.Set as Set

import Control.Monad (forM_, when)
import Data.ByteString.Lazy (ByteString)
import System.Directory (removeFile)
import System.Environment (getArgs)
import System.FilePath.Glob (glob)

help :: String
help = unlines
  [ "Convert JSON primitive files into YAML ones. YAML files will be written to "
  , "the original filename with '.yaml' appended."
  , ""
  , "Usage:"
  , "  v16-upgrade-primitives [options]... <file>..."
  , ""
  , "Options:"
  , "  --dry-run        Do not write YAML files."
  , "  --delete         Delete JSON files after writing."
  , "  --help | -h      Show this screen."
  , ""
  , "Example:"
  , "  v16-upgrade-primitives --dry-run prims/**/*.primitives"
  ]

-- | Same as 'glob', but errors on patterns matching no files.
globOrErr :: FilePath -> IO [FilePath]
globOrErr pattern = do
  files <- glob pattern
  when (null files) (error ("Pattern does not match any files: " <> pattern))
  pure files

-- | 'concatMap', but its monadic cousin
concatMapM :: Monad m => (a -> m [b]) -> [a] -> m [b]
concatMapM f = fmap concat . mapM f

-- | Read file and output YAML ByteString
jsonToYaml :: FilePath -> IO ByteString
jsonToYaml path = do
  contents <- ByteString.readFile path
  let decoded = AesonExtra.decodeOrErrJson path contents
  pure . removeTempKey . ByteString.fromStrict . Yaml.encode . customSortOutput $ decoded

{- NOTE [Sorting YAML object keys]

'Yaml.encode' encodes object with their keys in alphabetical order.
For readability we like `name` to be at the top, and `type` to be just above `template`.

We accomplice this here by renaming those keys to something there sorts where
we like them to be. And find-and-replace those temporary names back
in the resulting ByteString.
-}
keySortingRenames :: IsString str => [(str,str)]
keySortingRenames =
  [ ("name", "aaaa_really_should_be_name_but_renamed_to_get_the_sorting_we_like")
  , ("type", "really_should_be_type_but_renamed_to_get_the_sorting_we_like")
  ]

customSortOutput :: Aeson.Value -> Aeson.Value
customSortOutput x = case x of
  Aeson.Object o -> Aeson.Object $ fmap customSortOutput $ renameKeys $ o
  Aeson.Array xs -> Aeson.Array $ fmap customSortOutput xs
  _ -> x
 where
  renameKeys obj = foldl renameKey obj keySortingRenames
  renameKey obj (kOld,kNew) =
    case Aeson.lookup kOld obj of
      Nothing -> obj
      Just val -> Aeson.insert kNew val (Aeson.delete kOld obj)

removeTempKey :: ByteString -> ByteString
removeTempKey inp =
  LazyText.encodeUtf8 (foldl go (LazyText.decodeUtf8 inp) keySortingRenames)
 where
  go txt (orig,temp) = LazyText.replace temp orig txt

main :: IO ()
main = do
  args0 <- Set.fromList <$> getArgs

  let
    doDryRun = Set.member "--dry-run" args0
    doWrite = not doDryRun
    doDelete = Set.member "--delete" args0 && doWrite
    doHelp = Set.member "-h" args0 || Set.member "--help" args0 || Set.null args1
    args1 = foldr Set.delete args0 ["--dry-run", "--delete", "--help", "-h"]

  if doHelp then
    putStrLn help
  else do
    files <- concatMapM globOrErr (Set.toList args1)
    forM_ files $ \path -> do
      let newPath = path <> ".yaml"
      putStrLn $ "Converting " <> path <> ".."
      decoded <- jsonToYaml path
      when doWrite $ ByteString.writeFile newPath decoded
      when doDelete $ removeFile path