packages feed

literal-flake-input-0.0.4: src/LiteralFlakeInput/UrlEncoder.hs

{-# LANGUAGE MultilineStrings #-}
module LiteralFlakeInput.UrlEncoder where
import Data.Binary.Builder (fromByteString, toLazyByteString)
import Data.List.NonEmpty qualified as NL
import Data.Map.Strict qualified as M
import Data.Text qualified as T
import Data.Time.Clock ( getCurrentTime )
import Data.Version (showVersion)
import LiteralFlakeInput.Nix ( quoteUnquotedString )
import LiteralFlakeInput.Prelude
import Network.HTTP.Types (encodePathSegments)
import Nix
    ( Options,
      nixEvalExprLoc,
      normalForm,
      defaultOptions,
      parseNixTextLoc )
import Nix.Standard ( runWithBasicEffectsIO )
import Paths_literal_flake_input ( version )
import System.Environment.Blank (getEnvDefault)
import Text.PrettyPrint.Leijen.Text (linebreak, text, putDoc, vcat, indent)
import Text.Regex.TDFA ( (=~) )

runUrlEncoder :: IO ()
runUrlEncoder = runUrlEncoderWith . fmap toText =<< getArgs

runUrlEncoderWith :: [Text] -> IO ()
runUrlEncoderWith args
  | args `elem` [["--version"], ["-v"]] = do
    putStrLn $ "Literal Flake Input " <> showVersion version
    exitFailure
  | args `elem` [[], ["-h"], ["-help"], ["--help"], ["-help"], ["help"]] = do
    putStrLn """Literal Flake Input (LFI) URL encoder
      Usage: nix build $(e -an1 true \\
                           -an2 null \\
                           -an3 12 \\
                           -an4 hello world \\
                           -an5 [ 1 2 ] \\
                           -an6 "{x = 1; y = 2; }" \\
                           -an7 x: x + 1)

      The result is a non-flake URL input:
      nix build --override-input c https://lficom.me/an1/true/an2/null/an3/42/an4/%22hello%20world%22/an5/%5B%201%202%20%5D/an6/%7Bx%20=%201%3B%20y%20=%202%3B%20%7D/an7/a:%20a%20+%201/.tar

      LFI translates arbitrary command line arguments into a Nix attribute set.
      Nix values are verified with hnix interpreter.

      URL prefix can be overriden via environment variable LFI_SITE.
      If you copy URL into flake file as a default value then drop tar suffix.

      Project home page https://github.com/yaitskov/literal-flake-input"""
    exitFailure
  | length args == 1 = do
    putStrLn """LFI expects "even" number of argument. See help by -h or --help"""
    exitFailure
  | otherwise =
    case fmap (quoteUnquotedString . joinArgValueWords) <$> groupByAttrName args of
      Left e -> putStrLn (toString e) >> exitFailure
      Right m -> do
        time <- getCurrentTime
        let opts = defaultOptions time
        (goodAtrs, badAtrs) <- M.partition isRight <$> M.traverseWithKey (go opts) m
        if null badAtrs then
          do
            siteRoot <- toText <$> getEnvDefault "LFI_SITE" siteRootDef
            putLBSLn . mkLfiUrl siteRoot . M.mapKeys argToAtr $ M.mapMaybe rightToMaybe goodAtrs
          else
          do
            putDoc $ text "Invalid attribute(s):" <> linebreak <>
              idnt4 (vcat . mapMaybe leftToMaybe $ M.elems badAtrs) <>
              linebreak
            exitFailure
  where
    idnt4 = indent 4
    siteRootDef = "https://lficom.me"
    go opts (ArgName an) av = do
      validateNixExpr opts av >>= \case
        Left e -> pure . Left $
          (text (toLazy an) <> linebreak <> (idnt4 . vcat . fmap (text . toLazy) $ lines e))
        Right () -> pure $ Right av

newtype ArgName = ArgName Text deriving newtype (Eq, Show, Ord)
newtype AtrName = AtrName Text deriving newtype (Eq, Show, Ord)

argToAtr :: ArgName -> AtrName
argToAtr (ArgName s) = AtrName (T.drop 1 s)

isAtrName :: Text -> Bool
isAtrName = (=~ ("^[-][a-zA-Z_][a-zA-Z0-9_-]*[a-zA-Z0-9_]*$" :: Text))

groupByAttrName :: [Text] -> Either Text (Map ArgName (NonEmpty Text))
groupByAttrName [] = Right M.empty
groupByAttrName [a] = Left $ "Attribute [" <> a <> "] has no value"
groupByAttrName (atrN:argsLeft)
  | isAtrName atrN =
      case break isAtrName argsLeft of
        (atrVs, nextAtr) ->
          case nonEmpty atrVs of
            Nothing -> Left $ "Attribute [" <> atrN <> "] has no value"
            Just neAtrVs ->
              M.insert (ArgName atrN) neAtrVs <$> groupByAttrName nextAtr
  | otherwise = Left $ "Attribute name [" <> atrN <> "] is not valid"

joinArgValueWords :: NonEmpty Text -> Text
joinArgValueWords =  unwords . NL.toList


validateNixExpr :: Options -> Text -> IO (Either Text ())
validateNixExpr o nxe =
  case parseNixTextLoc nxe of
    Left e ->
      pure . Left $ "[" <> nxe <>  "] is bad Nix expression due:" <> show e
    Right e ->
      runWithBasicEffectsIO o $ do
        !_ <- getNormForm e
        pure $ Right ()
  where
    getNormForm = normalForm <=< nixEvalExprLoc mempty

mkLfiUrl :: Text -> Map AtrName Text -> LByteString
mkLfiUrl siteRoot w =
  toLazyByteString $
    "--override-input c " <>
    fromByteString (encodeUtf8 siteRoot) <>
    encodePathSegments (flatpairs $ M.toList w)
    <> "/.tar"
  where
   flatpairs :: [(AtrName, Text)] -> [Text]
   flatpairs [] = []
   flatpairs ((AtrName an, av):t) = an : av : flatpairs t