packages feed

elm-get-0.1: src/Get/Publish.hs

{-# LANGUAGE OverloadedStrings #-}
module Get.Publish where

import Control.Applicative ((<$>))
import Control.Monad (when)
import Control.Monad.Error
import System.Directory
import System.Exit
import System.FilePath (replaceExtension, (</>))
import System.IO
import qualified Data.Maybe as Maybe
import qualified Data.List as List
import qualified Data.Map as Map
import qualified Data.ByteString as BS
import Text.JSON
import qualified Utils.PrettyJson as Pretty

import Data.Version
import qualified Get.Registry              as R
import qualified Utils.Paths               as Path
import qualified Utils.Commands            as Cmd
import qualified Utils.Http                as Http
import qualified Elm.Internal.Dependencies as D
import qualified Elm.Internal.Paths        as EPath
import qualified Elm.Internal.Name         as N
import qualified Elm.Internal.Version      as V

publish :: ErrorT String IO ()
publish =
  do deps <- getDeps
     let name = D.name deps
         version = D.version deps
         exposedModules = D.exposed deps
     Cmd.out $ unwords [ "Verifying", show name, show version, "..." ]
     verifyNoDependencies (D.dependencies deps)
     verifyElmVersion (D.elmVersion deps)
     verifyExposedModules exposedModules
     verifyVersion name version
     withCleanup $ do
       generateDocs exposedModules
       R.register name version Path.combinedJson
     Cmd.out "Success!"

getDeps :: ErrorT String IO D.Deps
getDeps =
  do either <- liftIO $ runErrorT $ D.depsAt EPath.dependencyFile
     case either of
       Right deps -> return deps
       Left err ->
           liftIO $ do
             hPutStrLn stderr $ "\nError: " ++ err
             hPutStr stdout $ "\nWould you like me to add the missing fields? (y/n) "
             yes <- Cmd.yesOrNo
             case yes of
               False -> hPutStrLn stdout "Okay, maybe next time!"
               True -> do
                 addMissing =<< readFields
                 hPutStrLn stdout $ "Done! Now go through " ++ EPath.dependencyFile ++ 
                      " and check that\neach field is filled in with valid and helpful information."
             exitFailure
            
addMissing :: Map.Map String JSValue -> IO ()
addMissing existingFields =
    writeFile EPath.dependencyFile $ show $ Pretty.object obj'
    where
      obj' = map (\(f,v) -> (f, Maybe.fromMaybe v (Map.lookup f existingFields))) obj

      str = JSString . toJSString
      obj = [ ("version", str "0.1")
            , ("summary", str "concise, helpful summary of your project")
            , ("description", str "full description of this project, describe your use case")
            , ("license", str "BSD3")
            , ("repository", str "https://github.com/USER/PROJECT.git")
            , ("exposed-modules", JSArray [])
            , ("elm-version", str $ show V.elmVersion)
            , ("dependencies", JSObject $ toJSObject [])
            ]

readFields :: IO (Map.Map String JSValue)
readFields =
    do exists <- doesFileExist EPath.dependencyFile
       case exists of
         False -> return Map.empty
         True -> do raw <- readFile EPath.dependencyFile
                    case decode raw of
                      Error err -> return Map.empty
                      Ok obj -> return (Map.fromList $ fromJSObject obj)

withCleanup :: ErrorT String IO () -> ErrorT String IO ()
withCleanup action =
    do existed <- liftIO $ doesDirectoryExist "docs"
       either <- liftIO $ runErrorT action
       when (not existed) $ liftIO $ removeDirectoryRecursive "docs"
       case either of
         Left err -> throwError err
         Right () -> return ()

verifyNoDependencies :: [(N.Name,V.Version)] -> ErrorT String IO ()
verifyNoDependencies [] = return ()
verifyNoDependencies _ =
    throwError
        "elm-get is not able to publish projects with dependencies\n\
        \yet. This is obviously a very high proirity, and I am working as\n\
        \fast as I can! For now, let people know about your library on the\n\
        \mailing list: <https://groups.google.com/forum/#!forum/elm-discuss>"

verifyElmVersion :: V.Version -> ErrorT String IO ()
verifyElmVersion elmVersion@(V.V ns _)
    | ns == ns' = return ()
    | otherwise =
        throwError $ "elm_dependencies.json says this project depends on version " ++
                     show elmVersion ++ " of the compiler but the compiler you " ++
                     "have installed is version " ++ show V.elmVersion
    where
      V.V ns' _ = V.elmVersion

verifyExposedModules :: [String] -> ErrorT String IO ()
verifyExposedModules modules =
    do when (null modules) $ throwError $
              "There are no exposed modules in " ++ EPath.dependencyFile ++
              "!\nAll libraries must make at least one module available to users."
       mapM_ verifyExists modules
    where
      verifyExists modul =
          let path = Path.moduleToElmFile modul in
          do exists <- liftIO $ doesFileExist path
             when (not exists) $ throwError $
                 "Cannot find module " ++ modul ++ " at " ++ path

verifyVersion :: N.Name -> V.Version -> ErrorT String IO ()
verifyVersion name version =
    do response <- R.versions name
       case response of
         Nothing -> return ()
         Just versions ->
             do let maxVersion = maximum (version:versions)
                when (version < maxVersion) $ throwError $ unlines
                     [ "a later version has already been released."
                     , "Use a version number higher than " ++ show maxVersion ]
                checkSemanticVersioning maxVersion

       checkTag version

    where
      checkSemanticVersioning _ = return ()

      checkTag version = do
        tags <- lines <$> Cmd.git [ "tag", "--list" ]
        let v = show version
        when (show version `notElem` tags) $
             throwError (unlines (tagMessage v))

      tagMessage v =
          [ "Libraries must be tagged in git, but tag " ++ v ++ " was not found."
          , "These tags make it possible to find this specific version on github."
          , "To tag the most recent commit and push it to github, run this:"
          , ""
          , "    git tag -a " ++ v ++ " -m \"release version " ++ v ++ "\""
          , "    git push origin " ++ v
          , ""
          ]

generateDocs :: [String] -> ErrorT String IO ()
generateDocs modules = 
    do forM elms $ \path -> Cmd.run "elm-doc" [path]
       liftIO $ do
         let path = Path.combinedJson
         BS.writeFile path "[\n"
         let addCommas = List.intersperse (BS.appendFile path ",\n")
         sequence_ $ addCommas $ map append jsons
         BS.appendFile path "\n]"

    where
      elms = map Path.moduleToElmFile modules
      jsons = map Path.moduleToJsonFile modules

      append :: FilePath -> IO ()
      append path = do
        json <- BS.readFile path
        BS.length json `seq` return ()
        BS.appendFile Path.combinedJson json