literal-flake-input (empty) → 0.0.4
raw patch · 15 files changed
+1066/−0 lines, 15 filesdep +QuickCheckdep +basedep +binary
Dependencies added: QuickCheck, base, binary, ekg-core, ekg-wai, file-embed, hnix, http-types, literal-flake-input, monad-logger, optparse-applicative, ref-tf, regex-tdfa, relude, tagged, tar, tasty, tasty-discover, tasty-hunit, tasty-quickcheck, template-haskell, time, trace-embrace, unliftio, warp, warp-tls, wl-pprint-text, yesod-core
Files
- LICENSE +32/−0
- app/EncodeInputUrl.hs +7/−0
- app/LiteralFlakeInput.hs +8/−0
- changelog.md +12/−0
- literal-flake-input.cabal +253/−0
- src/LiteralFlakeInput/App.hs +10/−0
- src/LiteralFlakeInput/CmdArgs.hs +85/−0
- src/LiteralFlakeInput/CmdRun.hs +81/−0
- src/LiteralFlakeInput/Nix.hs +92/−0
- src/LiteralFlakeInput/Page.hs +290/−0
- src/LiteralFlakeInput/Prelude.hs +5/−0
- src/LiteralFlakeInput/UrlEncoder.hs +130/−0
- test/Discovery.hs +1/−0
- test/Driver.hs +13/−0
- test/LiteralFlakeInput/Test/Nix.hs +47/−0
+ LICENSE view
@@ -0,0 +1,32 @@+This module is under this "3 clause" BSD license:++Copyright (c) 2025-2025, Daniil Iaitskov+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are+met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * The names of the contributors may not be used to endorse or+ promote products derived from this software without specific+ prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ app/EncodeInputUrl.hs view
@@ -0,0 +1,7 @@+module Main where++import LiteralFlakeInput.Prelude+import LiteralFlakeInput.UrlEncoder++main :: IO ()+main = runUrlEncoder
+ app/LiteralFlakeInput.hs view
@@ -0,0 +1,8 @@+module Main where++import LiteralFlakeInput.CmdArgs+import LiteralFlakeInput.CmdRun+import LiteralFlakeInput.Prelude++main :: IO ()+main = execWithArgs runCmd
+ changelog.md view
@@ -0,0 +1,12 @@+# vpn-router changelog++## Version 0.0.4 2026-02-13+ * EKG monitoring+ * SSL certificates+ * **e** tool++## Version 0.0.3 2026-02-02+ * untarred output++## Version 0.0.1 2026-02-02+ * init
+ literal-flake-input.cabal view
@@ -0,0 +1,253 @@+cabal-version: 3.0+name: literal-flake-input+version: 0.0.4++synopsis: Web service converting URLs into archived nix attrsets+description:+ Service translates an HTTP GET path into a nix derivation that can be+ used as a flake input. Such a workaround provides the ability to emulate+ command line arguments in nix flakes.+ + In addtition to the web service there is a command line tool __e__+ (stands for escape\/encode). The e helps with URL encoding.+ + == Installation+ #installation#+ + === Service+ #service#+ + The service is already deployed at <https://lficom.me lficom.me>+ + === NixOS module+ #nixos-module#+ + ==== NixOS with flakes+ #nixos-with-flakes#+ + Modify @\/etc\/nixos\/flake.nix@ as follows:+ + > # ...+ > inputs = {+ > # ...+ > literal-flake-input.url = "github:yaitskov/literal-flake-input";+ > };+ + > # ...+ > modules = [+ > literal-flake-input.nixosModules.${system}.default+ > ({ ... }: {+ > programs.literal-flake-input {+ > enable = true;+ > port = 3000;+ > })+ > ./configuration.nix+ > ];+ + ==== NixOS without flakes+ #nixos-without-flakes#+ + > let+ > lfi = builtins.fetchGit "htts://github.com/yaitskov/literal-flake-input.git?ref=master";+ > in {+ > imports =+ > [ # ... ./hardware-configuration.nix+ > "${lfi}/nixos/non-flake-lfi.nix"+ > ];+ + > programs = {+ > literal-flake-input = {+ > port = 3000;+ > enable = true;+ > };+ > };+ + == How to use+ #how-to-use#+ + The project flake is using+ <https://github.com/yaitskov/literal-flake-input/blob/4342efaa6d4a74143b235f40873bd72f13c02cd3/flake.nix#L8 itself>.+ Another project using lfi is+ <https://github.com/yaitskov/vpn-router vpn-router>.+ + === Flake template+ #flake-template#+ + > # ...+ > inputs = {+ > nixpkgs.url = "github:NixOS/nixpkgs";+ > utils.url = "github:numtide/flake-utils";+ > c = {+ > url = "https://lficom.me/name/Bill/";+ > flake = false;+ > };+ > };+ > outputs = { self, nixpkgs, utils, c }:+ > utils.lib.eachDefaultSystem (sys:+ > let+ > pkgs = nixpkgs.legacyPackages.${sys};+ > cnf = import c { inherit pkgs; };+ > in+ > {+ > packages.hello =+ > pkgs.writeShellScriptBin+ > "hello"+ > "echo Hello ${cnf.name}";+ > });+ > # ...+ + === Overriding default values+ #overriding-default-values#+ + > nix build --override-input c \+ > https://lficom.me/name/Bob/.tar+ + There is a commant line tool __e__, that helps with boilerplates and+ input validation:+ + > nix build $(e -name Bob)+ + The tool supports literal keyword values (e.g. @true@ and @null@),+ strings, numbers, arrays and attribute sets. String quotation is+ optional. All values are parsed and evaluated by __e__ with nix to catch+ typos as soon as possible.+ + > 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 above command generates an input link which is going to be resolved+ by the service into:+ + > {...}: {+ > an1 = true;+ > an2 = null;+ > an3 = 12;+ > an4 = "hello wolrd";+ > an5 = [1 2];+ > an6 = {x = 1; y = 2; };+ > an7 = x: x + 1;+ > }+ + If you copy an URL generated by __e__ into a flake for default values,+ then drop @.tar@ suffix.+ + Alternative URL prefix can be set via environment variable @LFI_SITE@.+ + == Development environment+ #development-environment#+ + > $ nix develop+ > $ emacs &+ > $ cabal build+ > $ cabal test+ + > $ nix build+ > $ ./result/bin/literal-flake-input run+ > $ nix build $(./result/bin/e -static true)+homepage: http://github.com/yaitskov/literal-flake-input+license: BSD-3-Clause+license-file: LICENSE+author: Daniil Iaitskov+maintainer: dyaitskov@gmail.com+copyright: Daniil Iaitkov 2026+category: System+build-type: Simple+bug-reports: https://github.com/yaitskov/literal-flake-input/issues+extra-doc-files:+ changelog.md+tested-with:+ GHC == 9.12.2++source-repository head+ type:+ git+ location:+ https://github.com/yaitskov/literal-flake-input.git++common base+ default-language: GHC2024+ ghc-options: -Wall+ default-extensions:+ DefaultSignatures+ NoImplicitPrelude+ OverloadedStrings+ TemplateHaskell+ build-depends:+ base >=4.7 && < 5+ , optparse-applicative < 1+ , relude >= 1.2.2 && < 2+ , tagged < 1+ , unliftio < 1+ , yesod-core < 1.8++library+ import: base+ hs-source-dirs: src+ exposed-modules:+ LiteralFlakeInput.App+ LiteralFlakeInput.CmdArgs+ LiteralFlakeInput.CmdRun+ LiteralFlakeInput.Nix+ LiteralFlakeInput.Page+ LiteralFlakeInput.Prelude+ LiteralFlakeInput.UrlEncoder+ other-modules:+ Paths_literal_flake_input+ autogen-modules:+ Paths_literal_flake_input+ build-depends:+ , binary < 1+ , ekg-core < 1+ , ekg-wai < 1+ , file-embed < 1+ , hnix < 1+ , http-types < 1+ , monad-logger < 1+ , ref-tf < 1+ , regex-tdfa < 2+ , tar < 1+ , template-haskell < 3+ , time < 2+ , trace-embrace < 2+ , warp < 4+ , warp-tls < 4+ , wl-pprint-text < 2++test-suite test+ import: base+ type: exitcode-stdio-1.0+ main-is: Driver.hs+ other-modules:+ LiteralFlakeInput.Test.Nix+ Discovery+ hs-source-dirs:+ test+ ghc-options: -Wall -rtsopts -threaded -main-is Driver+ build-depends:+ , literal-flake-input+ , QuickCheck+ , tasty+ , tasty-discover+ , tasty-hunit+ , tasty-quickcheck++executable literal-flake-input+ import: base+ ghc-options: -Wall -threaded "-with-rtsopts=-T -N"+ main-is: LiteralFlakeInput.hs+ hs-source-dirs: app+ build-depends:+ , literal-flake-input++executable e+ import: base+ ghc-options: -Wall -threaded "-with-rtsopts=-N"+ main-is: EncodeInputUrl.hs+ hs-source-dirs: app+ build-depends:+ , literal-flake-input
+ src/LiteralFlakeInput/App.hs view
@@ -0,0 +1,10 @@+module LiteralFlakeInput.App where++import LiteralFlakeInput.Prelude+ ( ($), String, MonadIO, HasCallStack )+import UnliftIO.Exception ( stringException, throwIO )++type NetM m = (HasCallStack, MonadIO m)++ex :: NetM m => String -> m a+ex em = throwIO $ stringException em
+ src/LiteralFlakeInput/CmdArgs.hs view
@@ -0,0 +1,85 @@+module LiteralFlakeInput.CmdArgs where++import Options.Applicative+import LiteralFlakeInput.Prelude+++data HttpPort+data EkgPort+data Cert+data CertKey++data CmdArgs+ = RunService+ { httpPortToListen :: Tagged HttpPort Int+ , certFile :: Maybe (Tagged Cert FilePath)+ , keyFile :: Maybe (Tagged CertKey FilePath)+ , ekgPort :: Maybe (Tagged EkgPort Int)+ }+ | LiteralFlakeInputVersion+ deriving (Eq, Show)++execWithArgs :: MonadIO m => (CmdArgs -> m a) -> m a+execWithArgs a = a =<< liftIO (execParser $ info (cmdp <**> helper) phelp)+ where+ serviceP = RunService <$> portOption <*> certO <*> certKeyO <*> ekgPortOption+ cmdp =+ hsubparser+ ( command "run" (infoP serviceP "launch the service exposed over HTTP")+ <> command "version" (infoP (pure LiteralFlakeInputVersion) "print program version"))++ infoP p h = info p (progDesc h <> fullDesc)+ phelp =+ progDesc+ "Translates HTTP GET params into Nix attrset"++portOption :: Parser (Tagged HttpPort Int)+portOption = Tagged <$>+ option auto+ ( long "port"+ <> short 'p'+ <> showDefault+ <> value 3042+ <> help "HTTP port to listen"+ <> metavar "PORT"+ )++ekgPortOption :: Parser (Maybe (Tagged EkgPort Int))+ekgPortOption = pured . zeroToNothing <$>+ option auto+ ( long "ekg-port"+ <> short 'e'+ <> value 0+ <> help "HTTP port for EKG monitoring. Zero to disable."+ <> metavar "EKG"+ )++emptyToNothing :: FilePath -> Maybe FilePath+emptyToNothing "" = Nothing+emptyToNothing s = Just s++zeroToNothing :: Int -> Maybe Int+zeroToNothing s | s <= 0 = Nothing+ | otherwise = Just s++pured :: (Applicative g, Applicative f) => g a -> g (f a)+pured = fmap pure++certO :: Parser (Maybe (Tagged Cert FilePath))+certO = pured . emptyToNothing <$>+ strOption+ ( long "certificate"+ <> short 'c'+ <> value ""+ <> help "path to SSL certificate file (./certificate.pem)"+ <> metavar "CERT"+ )+certKeyO :: Parser (Maybe (Tagged CertKey FilePath))+certKeyO = pured . emptyToNothing <$>+ strOption+ ( long "key"+ <> short 'k'+ <> value ""+ <> help "path to key file of SSL certificate (./key.pem)"+ <> metavar "KEY"+ )
+ src/LiteralFlakeInput/CmdRun.hs view
@@ -0,0 +1,81 @@+{-# LANGUAGE OverloadedRecordDot #-}+module LiteralFlakeInput.CmdRun where++import Control.Monad.Logger ( liftLoc, ToLogStr(toLogStr) )+import Data.Version (showVersion)+import Language.Haskell.TH.Syntax (qLocation)+import LiteralFlakeInput.Page ( Ypp(Ypp), YppMetrics (YppMetrics) )+import LiteralFlakeInput.CmdArgs ( CmdArgs(..), CertKey, Cert )+import LiteralFlakeInput.Prelude+import Network.Wai.Handler.WarpTLS ( runTLS, tlsSettings, TLSSettings )+import Network.Wai.Handler.Warp+ ( Settings,+ setBeforeMainLoop,+ setMaxTotalHeaderLength,+ setOnException,+ setPort,+ setServerName,+ setSlowlorisSize,+ setTimeout,+ runSettings,+ defaultSettings,+ defaultShouldDisplayException )+import Paths_literal_flake_input ( version )+import System.Metrics ( createCounter, newStore, registerGcMetrics )+import System.Remote.Monitoring.Wai ( forkServerWith )+import Yesod.Core+ ( toWaiApp,+ LogLevel(LevelError),+ Application,+ Yesod(makeLogger, messageLoggerSource) )+import Yesod.Core.Types ( Logger )++mkSettings :: Ypp -> CmdArgs -> Logger -> Settings+mkSettings yp ca logger =+ setPort port $+ setServerName "LiteralFlakeInput" $+ setOnException onEx $+ setSlowlorisSize 1024 $+ setMaxTotalHeaderLength 1024 $+ setBeforeMainLoop+ (putStrLn $ "Go http://localhost:" <> show port <> "/hello/42/prod/true/plugin/null/name/Alice") $+ setTimeout 9+ defaultSettings++ where+ port = untag ca.httpPortToListen+ shouldLog' = defaultShouldDisplayException+ onEx _ e =+ when (shouldLog' e) $+ messageLoggerSource+ yp+ logger+ $(qLocation >>= liftLoc)+ "yesod-core"+ LevelError+ (toLogStr $ "Exception from Warp: " ++ show e)++mkTlsSettings :: Tagged Cert FilePath -> Tagged CertKey FilePath -> TLSSettings+mkTlsSettings cert key = tlsSettings (untag cert) (untag key)++runPlain :: Settings -> Application -> IO ()+runPlain = runSettings++runCmd :: CmdArgs -> IO ()+runCmd = \case+ rs@RunService {} -> do+ st <- newStore+ registerGcMetrics st+ myMetrics <- YppMetrics <$> createCounter "landing.requests" st <*> createCounter "api.requests" st+ forM_ rs.ekgPort $ \(Tagged ep) -> do+ putStrLn $ "Launch EKG on port " <> show ep+ forkServerWith st "0.0.0.0" ep++ $(trIo "start/rs")+ let y = Ypp st myMetrics+ logger <- makeLogger y+ case liftA2 mkTlsSettings rs.certFile rs.keyFile of+ Nothing -> runPlain (mkSettings y rs logger) =<< toWaiApp y+ Just tlsSngs -> runTLS tlsSngs (mkSettings y rs logger) =<< toWaiApp y+ LiteralFlakeInputVersion ->+ putStrLn $ "Version " <> showVersion version
+ src/LiteralFlakeInput/Nix.hs view
@@ -0,0 +1,92 @@+module LiteralFlakeInput.Nix where++import Data.Text.Lazy ( concat )+import Codec.Archive.Tar ( write )+import Codec.Archive.Tar.Entry ( fileEntry, toTarPath, TarPath )+import LiteralFlakeInput.Prelude+ ( otherwise,+ show,+ ($),+ (<$>),+ Eq,+ Show,+ Semigroup((<>)),+ Bool(False),+ Either(Left, Right),+ Text,+ LazyStrict(toLazy),+ uncurry,+ (.),+ LText,+ error,+ ConvertUtf8(encodeUtf8),+ LByteString,+ Bool(True),+ ToText(toText) )+import Text.Regex.TDFA ( (=~) )+import Yesod.Core+ ( ToTypedContent(..),+ TypedContent(TypedContent),+ ToContent(..),+ ContentType )++data OutFormat = TaredNix | PlainNix deriving (Show, Eq)+newtype NixDer (o :: OutFormat) = NixDer [(Text, Text)] deriving (Show, Eq)++instance ToContent (NixDer PlainNix) where+ toContent = toContent . renderNixDer++instance ToContent (NixDer TaredNix) where+ toContent = toContent . mkTar . renderNixDer++instance ToTypedContent (NixDer PlainNix) where+ toTypedContent = TypedContent xPlainNix . toContent++instance ToTypedContent (NixDer TaredNix) where+ toTypedContent = TypedContent xTarMime . toContent++pairs :: [a] -> [(a, a)]+pairs (a:b:t) = (a, b) : pairs t+pairs [_] = []+pairs [] = []++xTarMime :: ContentType+xTarMime = "application/x-tar"++xPlainNix :: ContentType+xPlainNix = "text/x-nix"++defaultNixName :: TarPath+defaultNixName =+ case toTarPath False "default.nix" of+ Right v -> v+ Left e -> error $ toText e++mkTar :: LText -> LByteString+mkTar txt = write [e]+ where+ e = fileEntry defaultNixName $ encodeUtf8 txt++{- HLINT ignore "Use concatMap" -}+renderNixDer :: NixDer o -> LText+renderNixDer (NixDer m) =+ "{...}:{" <> concat (uncurry renderEntry <$> m) <> "}"++isUnquotedString :: Text -> Bool+isUnquotedString s+ | s =~ ("^(true|false|null|-?[0-9]+([.][0-9]+)?|\".*\"|[[].*|[{].*|[a-zA-Z_][a-zA-Z0-9_-]*[a-zA-Z0-9_]*:.*)$" :: Text) = False+ | otherwise = True++quoteString :: Text -> Text+quoteString = show++quoteUnquotedString :: Text -> Text+quoteUnquotedString s+ | isUnquotedString s = quoteString s+ | otherwise = s++renderEntry :: Text -> Text -> LText+renderEntry k v = toLazy k <> "=" <> toLazy (quoteUnquotedString v) <> ";"++translate :: [Text] -> NixDer o+translate = NixDer . pairs
+ src/LiteralFlakeInput/Page.hs view
@@ -0,0 +1,290 @@+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}+{-# LANGUAGE MultilineStrings #-}+module LiteralFlakeInput.Page where++import Data.Binary.Builder (fromByteString)+import Data.ByteString qualified as BS+import Data.FileEmbed ( embedFile, makeRelativeToProject )+import Data.Text (isSuffixOf)+import Data.Version (showVersion)+import LiteralFlakeInput.Nix+ ( translate, OutFormat(PlainNix, TaredNix), NixDer(..))+import LiteralFlakeInput.Prelude+import Paths_literal_flake_input ( version )+import System.Metrics ( Store )+import Yesod.Core+ ( Yesod(defaultLayout, makeSessionBackend),+ notFound,+ getYesod,+ RenderRoute(renderRoute),+ ToTypedContent(..),+ TypedContent(TypedContent),+ Html,+ Content(ContentBuilder),+ ToContent(..),+ ToWidgetHead(toWidgetHead),+ preEscapedToMarkup,+ Texts,+ mkYesod,+ parseRoutes,+ logInfo,+ addContentDispositionFileName,+ setTitle,+ hamlet,+ lucius,+ whamlet,+ typeXml,+ typePlain,+ typeSvg, notFound )++import System.Metrics.Counter ( Counter, inc )++data YppMetrics+ = YppMetrics+ { landingPage :: Counter+ , apiCall :: Counter+ }++data Ypp+ = Ypp+ { metricsStore :: Store+ , yppMetrics :: YppMetrics+ }++mkYesod "Ypp" [parseRoutes|+/*{Texts} HomeR GET+|]++instance Yesod Ypp where+ makeSessionBackend _ = pure Nothing++data Contentable = forall x. (ToContent x, ToTypedContent x) => Contentable x+instance ToContent Contentable where+ toContent (Contentable x) = toContent x++instance ToTypedContent Contentable where+ toTypedContent (Contentable x) = toTypedContent x++newtype FavIcon = FavIcon ByteString++instance ToContent FavIcon where+ toContent (FavIcon bs) =+ ContentBuilder (fromByteString bs) (Just . fromIntegral $ BS.length bs)+instance ToTypedContent FavIcon where+ toTypedContent = TypedContent typeSvg . toContent++getFavIcon :: Handler FavIcon+getFavIcon = pure $ FavIcon $(makeRelativeToProject "assets/favicon.svg" >>= embedFile)++getGithubIcon :: Handler FavIcon+getGithubIcon = pure $ FavIcon $(makeRelativeToProject "assets/github.svg" >>= embedFile)++getSiteMap :: Handler TypedContent+getSiteMap = pure . TypedContent typeXml $ toContent $(makeRelativeToProject "assets/sitemap.xml" >>= embedFile)++getRobots :: Handler TypedContent+getRobots = pure . TypedContent typePlain $ toContent $(makeRelativeToProject "assets/robots.txt" >>= embedFile)++-- getRobots :: Handler TypedContent+-- getRobots = pure . TypedContent typePlain $ toContent "" $(makeRelativeToProject "assets/robots.txt" >>= embedFile)++getHomeR :: Texts -> Handler Contentable+getHomeR params =+ case nonEmpty params of+ Nothing -> Contentable <$> landing+ Just (p :| []) ->+ case p of+ "favicon.svg" -> Contentable <$> getFavIcon+ "github.svg" -> Contentable <$> getGithubIcon+ "robots.txt" -> Contentable <$> getRobots+ "sitemap.xml" -> Contentable <$> getSiteMap+ "version" ->+ pure . Contentable . TypedContent typePlain . toContent $+ "Literal Flake Input " <> showVersion version+ "index.html" -> Contentable <$> landing+ _ -> notFound+ Just p | ".tar" `isSuffixOf` last p ->+ trans (Proxy @TaredNix) "file.tar"+ | otherwise ->+ trans (Proxy @PlainNix) "default.nix"+ where+ trans :: forall tw. ToTypedContent (NixDer tw) => Proxy tw -> Text -> Handler Contentable+ trans _ fileName = do+ yp <- getYesod+ liftIO (inc $ apiCall $ yppMetrics yp)+ $(logInfo) $ "Translate " <> show params+ addContentDispositionFileName fileName $> Contentable (translate params :: NixDer tw)++generatedNixExpr :: Text+generatedNixExpr =+ """+ { ... }:+ {+ name = "Alice Wonder";+ age = 18;+ alive = true;+ job = null;+ }+ """++nixAndE :: Text+nixAndE = """nix build $(e -name Bob)"""++fullTypeRangeOfE :: Text+fullTypeRangeOfE =+ """+ nix build $(e -an1 true \\+ -an2 null \\+ -an3 12 \\+ -an4 hello world \\+ -an5 [ 1 2 ] \\+ -an6 "{x = 1; y = 2; }" \\+ -an7 x: x + 1)+ """++fullTypeRangeInputBody :: Text+fullTypeRangeInputBody =+ """+ {...}: {+ an1 = true;+ an2 = null;+ an3 = 12;+ an4 = "hello wolrd";+ an5 = [1 2];+ an6 = {x = 1; y = 2; };+ an7 = x: x + 1;+ }+ """++nixFlakeTemplate :: Text+nixFlakeTemplate =+ """+ # ...+ inputs = {+ nixpkgs.url = "github:NixOS/nixpkgs";+ utils.url = "github:numtide/flake-utils";+ c = {+ url = "https://lficom.me/static/false/";+ flake = false;+ };+ };+ outputs = { self, nixpkgs, utils, c }:+ utils.lib.eachDefaultSystem (sys:+ let+ pkgs = nixpkgs.legacyPackages.${sys};+ cnf = import c { inherit pkgs; };+ in+ {+ packages.hello =+ pkgs.writeShellScriptBin+ "hello"+ "echo Hello ${cnf.name}";+ });+ # ...+ """++nixBuildWithOverride :: Text+nixBuildWithOverride =+ """+ nix build --override-input c \\+ https://lficom.me/name/Bob/.tar+ """++landing :: Handler Html+landing = do+ yp <- getYesod+ liftIO (inc $ landingPage $ yppMetrics yp)+ defaultLayout $ do+ setTitle "Literal Flake Input"+ toWidgetHead+ [hamlet|+ <meta charset="utf-8" />+ <meta name="viewport" content="width=device-width, initial-scale=1" />+ <meta name="author" content="Daniil Iaitskov" />+ <meta name="keywords" content="nix flake input arguments nixpkgs derivation" />+ <meta name="description" content="Micro-service helps to pass argments to nix flakes as in nixpkgs" />+ <link rel="shortcut icon" href="favicon.svg" type="image/x-icon">+ |]+ toWidgetHead+ [lucius|+ body {+ background: #7CED53;+ background: radial-gradient(circle, rgb(182 213 217) 0%, rgb(205 255 253) 80%, rgb(206 214 255) 100%);+ padding: 0px 4%;+ color: #171717;+ }+ img.github {+ width: 8vh;+ opacity: 0.8;+ padding-top: 5vh;+ }+ h1 a {+ font-size: x-large;+ text-decoration: none;+ color: #e64d4d;+ }+ pre {+ font-size: small;+ }+ .wrap {+ white-space: pre-wrap;+ text-indent: -2em;+ padding-left: 2em;+ }+ |]+ [whamlet|+ <h1>+ <a href="https://lficom.me">+ Literal Flake Input+ <p>+ Service translates an HTTP GET path into a nix derivation+ that can be used as a flake input. Such a workaround provides+ the ability to emulate command line arguments in nix flakes.+ <pre class=wrap>+ wget -q -O - 'https://lficom.me/name/Alice Wonder/age/18/alive/true/job/null/'+ <h5>+ Response body+ <pre>+ #{preEscapedToMarkup generatedNixExpr}++ <p>+ nixpkgs derivations can accept command line arguments but nix flakes don't.+ <h5>+ Flake template+ <pre>+ #{preEscapedToMarkup nixFlakeTemplate}+ <h5>+ Override+ <pre>+ #{preEscapedToMarkup nixBuildWithOverride}+ <p>+ There is a commant line tool <b>e</b>, that helps with boilerplates and+ input validation:+ <pre>+ #{preEscapedToMarkup nixAndE}+ <p>+ The tool supports literal keyword values (e.g. <i>true</i> and <i>null</i>),+ strings, numbers, arrays and attribute sets. String quotation is+ optional. All values are parsed and evaluated by <b>e</b> with nix to catch typos+ as soon as possible.+ <pre>+ #{preEscapedToMarkup fullTypeRangeOfE}+ <p>+ The above command generates an input link which is going to be resolved+ by the service into:+ <pre>+ #{preEscapedToMarkup fullTypeRangeInputBody}+ <p>+ If you copy an URL generated by <b>e</b> into a flake for default+ values, then drop <i>.tar</i> suffix.+ <p>+ Alternative URL prefix can be set via environment variable <i>LFI_SITE</i>.++ <center>+ <p>+ <a href="https://github.com/yaitskov/literal-flake-input">+ <img class=github src=/github.svg />++ |]
+ src/LiteralFlakeInput/Prelude.hs view
@@ -0,0 +1,5 @@+module LiteralFlakeInput.Prelude (module X) where++import Data.Tagged as X+import Relude as X hiding (Handle, intercalate)+import Debug.TraceEmbrace as X hiding (a)
+ src/LiteralFlakeInput/UrlEncoder.hs view
@@ -0,0 +1,130 @@+{-# 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
+ test/Discovery.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF tasty-discover -optF --generated-module=Discovery #-}
+ test/Driver.hs view
@@ -0,0 +1,13 @@+module Driver where++import qualified Discovery+import Relude+import Test.Tasty++main :: IO ()+main = defaultMain =<< testTree+ where+ testTree :: IO TestTree+ testTree = do+ tests <- Discovery.tests+ pure $ testGroup "literal-flake-input" [ tests ]
+ test/LiteralFlakeInput/Test/Nix.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE MultilineStrings #-}+module LiteralFlakeInput.Test.Nix where++import Test.Tasty ( TestTree, testGroup )+import Test.Tasty.HUnit+import LiteralFlakeInput.Prelude+import LiteralFlakeInput.Nix++test_render :: TestTree+test_render =+ testGroup "render"+ [ testGroup "renderEntry"+ [ testCase "nat"+ $ renderEntry "a" "1" @?= "a=1;"+ , testCase "bool"+ $ renderEntry "a" "true" @?= "a=true;"+ , testCase "false"+ $ renderEntry "a" "false" @?= "a=false;"+ , testCase "null"+ $ renderEntry "a" "null" @?= "a=null;"+ , testCase "string"+ $ renderEntry "a" "hello world" @?= "a=\"hello world\";"+ , testCase "esc double quote"+ $ renderEntry "a" "hello \" world" @?= "a=\"hello \\\" world\";"+ , testCase "esc left slash"+ $ renderEntry "a" "hello \\" @?= "a=\"hello \\\\\";"+ , testCase "quoted"+ $ renderEntry "a" "\"hello\"" @?= "a=\"hello\";"+ , testCase "list"+ $ renderEntry "a" "[1 2]" @?= "a=[1 2];"+ , testCase "attr-set"+ $ renderEntry "a" "{x = 1; y = 2}" @?= "a={x = 1; y = 2};"+ , testCase "lambda"+ $ renderEntry "a" "x: x + 1" @?= "a=x: x + 1;"+ , testCase "empty"+ $ renderEntry "a" "" @?= "a=\"\";"+ ]+ , testGroup "renderNixDer"+ [ testCase "empty"+ $ renderNixDer (NixDer []) @?= """{...}:{}"""+ , testCase "one"+ $ renderNixDer (NixDer [("key", "value")]) @?= """{...}:{key="value";}"""+ , testCase "two"+ $ renderNixDer (NixDer [("key", "value"), ("key2", "value2")])+ @?= """{...}:{key="value";key2="value2";}"""+ ]+ ]