packages feed

project-m36-0.9.4: src/bin/TutorialD/Interpreter/Import/TutorialD.hs

module TutorialD.Interpreter.Import.TutorialD where
import ProjectM36.Base
import TutorialD.Interpreter.Import.Base
import TutorialD.Interpreter.Base hiding (try)
import TutorialD.Interpreter.DatabaseContextExpr
import ProjectM36.Error
import qualified ProjectM36.Error as PM36E
import qualified Data.Text as T
import Control.Exception
import qualified Data.ByteString as BS
import qualified Data.Text.Encoding as TE
import qualified Text.URI as URI
import Text.URI (URI)
import Crypto.Hash.SHA256
import Data.ByteString.Base16 as B16
import Network.HTTP.Simple
import Network.HTTP.Types
--import a file containing TutorialD database context expressions via local files or HTTP/HTTPS

importTutorialDFromFile :: URI -> HashVerification -> IO (Either RelationalError DatabaseContextExpr)
importTutorialDFromFile fileURI mHash = do
  case URI.uriPath fileURI of
    Just (False, _) -> do
      eTutdBytes <- try (BS.readFile filePath) :: IO (Either IOError BS.ByteString)
      case eTutdBytes of 
        Left err -> pure $ Left (ImportError (ImportFileError (T.pack (show err))))
        Right tutdBytes ->
          case validateBytes tutdBytes mHash of
            Left err -> pure (Left err)
            Right tutdBytes' -> importTutorialDBytes tutdBytes'
    _ -> pure (Left (ImportError (InvalidFileURIError (URI.render fileURI))))
  where
    filePath = drop (length ("file://" :: String)) (URI.renderStr fileURI)
        

importTutorialDBytes :: BS.ByteString -> IO (Either RelationalError DatabaseContextExpr)
importTutorialDBytes tutdBytes =
  case TE.decodeUtf8' tutdBytes of
    Left err -> pure (Left (ImportError (ImportFileDecodeError (T.pack (show err)))))
    Right tutdUtf8 -> 
      case parse (multipleDatabaseContextExprP <* eof) "import" tutdUtf8 of
        --parseErrorPretty is new in megaparsec 5
        Left err -> pure (Left (PM36E.ParseError (T.pack (errorBundlePretty err))))
        Right expr -> pure (Right expr)

importTutorialDViaHTTP :: URI -> HashVerification -> IO (Either RelationalError DatabaseContextExpr)
importTutorialDViaHTTP uri mHash = do
  reqURI <- parseRequest (URI.renderStr uri)
  response <- httpBS reqURI
  let scode = getResponseStatus response
  if scode == ok200 then
      let byteBody = getResponseBody response in
      case validateBytes byteBody mHash of
        Right byteBody' -> do
          importTutorialDBytes byteBody'
        Left err -> pure (Left err)
    else
      pure (Left (ImportError (ImportDownloadError (T.pack ("download failed: " <> show scode)))))

validateBytes :: BS.ByteString -> HashVerification -> Either RelationalError BS.ByteString
validateBytes tutdBytes mHash =
  case mHash of
    Nothing -> pure tutdBytes
    Just expectedHashText ->
      case B16.decode (TE.encodeUtf8 expectedHashText) of
        Left _ -> Left (ImportError (InvalidSHA256Error expectedHashText))
        Right expectedHash | hashBytes == expectedHash -> pure tutdBytes
                           | otherwise ->
                             let p = TE.decodeUtf8 . B16.encode in
                             Left (ImportError (SHA256MismatchError (p expectedHash) (p hashBytes)))
 where
    hashBytes = hash tutdBytes

    
tutdImportP :: Parser DatabaseContextDataImportOperator
tutdImportP = do
  reserved ":importtutd" 
  uri <- quoted URI.parser
  spaceConsumer
  mhash <- optional (quoted hex)
  handler <- case URI.unRText <$> URI.uriScheme uri of
               Nothing -> fail "URI scheme missing"
               Just "file" -> pure importTutorialDFromFile
               Just scheme | scheme `elem` ["http", "https"] -> pure importTutorialDViaHTTP
                           | otherwise -> fail ( "unsupported URI scheme: " <> show scheme)
  pure $ DatabaseContextDataImportOperator uri mhash handler