effectful-config-file-secretspec (empty) → 0.1.0.0
raw patch · 5 files changed
+219/−0 lines, 5 filesdep +basedep +containersdep +effectful
Dependencies added: base, containers, effectful, effectful-config-file, effectful-config-file-secretspec, filepath, hspec, lens, secretspec, temporary, text
Files
- README.md +27/−0
- effectful-config-file-secretspec.cabal +66/−0
- src/Effectful/Config/Secrets/SecretSpec.hs +36/−0
- test/Effectful/Config/Secrets/SecretSpecSpec.hs +88/−0
- test/Spec.hs +2/−0
+ README.md view
@@ -0,0 +1,27 @@+# effectful-config-file-secretspec++SecretSpec-backed Secrets effect handler for effectful-config-file.++## Installing libsecretspec++The `secretspec` Haskell package links against the libsecretspec C library,+found through `pkg-config`. Build it from the+[secretspec](https://github.com/cachix/secretspec) tag matching the+`secretspec` extra-dep in `stack.yaml`.++Requirements: [rustup](https://rustup.rs), `pkg-config` and+[cargo-c](https://github.com/lu-zero/cargo-c) (`cargo install cargo-c`).++```bash+git clone --depth 1 --branch v0.21.1 https://github.com/cachix/secretspec.git+cd secretspec+cargo cinstall -p libsecretspec --manifest-path Cargo.toml \+ --library-type staticlib \+ --prefix "$HOME/.libsecretspec" \+ --bindir lib \+ --libdir lib \+ --pkgconfigdir lib/pkgconfig+export PKG_CONFIG_PATH="$HOME/.libsecretspec/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"+```++Check it with `pkg-config --libs libsecretspec`, then build with `stack build`.
+ effectful-config-file-secretspec.cabal view
@@ -0,0 +1,66 @@+cabal-version: 2.2++-- This file has been generated from package.yaml by hpack version 0.39.6.+--+-- see: https://github.com/sol/hpack++name: effectful-config-file-secretspec+version: 0.1.0.0+synopsis: SecretSpec backend for effectful-config-file+description: SecretSpec-backed Secrets effect handler for effectful-config-file+category: Configuration+author: drlkf+maintainer: drlkf@drlkf.net+copyright: 2026 drlkf+license: BSD-3-Clause+build-type: Simple+extra-source-files:+ README.md++library+ exposed-modules:+ Effectful.Config.Secrets.SecretSpec+ other-modules:+ Paths_effectful_config_file_secretspec+ autogen-modules:+ Paths_effectful_config_file_secretspec+ hs-source-dirs:+ src+ ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints+ build-tool-depends:+ hspec-discover:hspec-discover+ build-depends:+ base >=4.7 && <5+ , containers >=0.2 && <1+ , effectful >=2.6 && <3+ , effectful-config-file >=0.1 && <2+ , secretspec >=0.21 && <1+ , text >=2.1 && <3+ default-language: Haskell2010++test-suite effectful-config-file-secretspec-test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ Effectful.Config.Secrets.SecretSpecSpec+ Paths_effectful_config_file_secretspec+ autogen-modules:+ Paths_effectful_config_file_secretspec+ hs-source-dirs:+ test+ ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N+ build-tool-depends:+ hspec-discover:hspec-discover+ build-depends:+ base >=4.7 && <5+ , containers >=0.2 && <1+ , effectful >=2.6 && <3+ , effectful-config-file >=0.1 && <2+ , effectful-config-file-secretspec+ , filepath+ , hspec+ , lens+ , secretspec >=0.21 && <1+ , temporary+ , text >=2.1 && <3+ default-language: Haskell2010
+ src/Effectful/Config/Secrets/SecretSpec.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE TypeOperators #-}++module Effectful.Config.Secrets.SecretSpec (+ runSecretSpec,+) where++import qualified Data.Map.Strict as Map+import Effectful (Eff, IOE, liftIO, type (:>))+import Effectful.Config.Secrets (Secrets (..), SecretsError (..))+import Effectful.Dispatch.Dynamic (interpret)+import Effectful.Error.Static (Error, throwError)+import Effectful.Exception (bracket)+import SecretSpec (Builder, builder, close, get, load, resolvedSecrets)++-- | @since 0.1.0.0+runSecretSpec+ :: IOE :> es+ => Error SecretsError :> es+ => (Builder -> Builder)+ -> Eff (Secrets : es) a+ -> Eff es a+runSecretSpec f act =+ bracket (liftIO (load (f builder))) (liftIO . close) $ \r ->+ interpret+ ( \_ -> \case+ GetSecret k ->+ maybe+ (throwError (SecretNotFound k))+ pure+ (Map.lookup k (resolvedSecrets r) >>= get)+ )+ act
+ test/Effectful/Config/Secrets/SecretSpecSpec.hs view
@@ -0,0 +1,88 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeApplications #-}++module Effectful.Config.Secrets.SecretSpecSpec (+ spec,+) where++import Control.Concurrent.MVar (newMVar)+import Control.Lens (makeLenses)+import Data.Config (ConfigValue (..))+import qualified Data.Text as T (pack)+import Effectful (runEff)+import Effectful.Config (getConfigValue)+import Effectful.Config.Log (runLog)+import Effectful.Config.Secrets (SecretsError (..), getSecret)+import Effectful.Config.Secrets.SecretSpec (runSecretSpec)+import Effectful.Error.Static (runErrorNoCallStack)+import Effectful.Reader.Static (runReader)+import SecretSpec (Builder, withPath, withProvider, withReason)+import System.FilePath ((</>))+import System.IO.Temp (withSystemTempDirectory)+import Test.Hspec (Spec, describe, it, shouldBe)++newtype SecretConfig = SecretConfig+ { _secret_val :: ConfigValue String+ }+ deriving (Eq, Show)++makeLenses ''SecretConfig++silentLog :: a -> b -> c -> d -> IO ()+silentLog _ _ _ _ = pure ()++withManifest :: (FilePath -> (Builder -> Builder) -> IO a) -> IO a+withManifest act = withSystemTempDirectory "secretspec" $ \dir -> do+ writeFile (dir </> ".env") "MY_SECRET=hunter2\n"+ writeFile+ (dir </> "secretspec.toml")+ ( unlines+ [ "[project]"+ , "name = \"test\""+ , "revision = \"1.0\""+ , ""+ , "[profiles.default]"+ , "MY_SECRET = { description = \"test\", required = true }"+ ]+ )+ act+ dir+ ( withPath (T.pack (dir </> "secretspec.toml"))+ . withProvider (T.pack ("dotenv://" <> (dir </> ".env")))+ . withReason "test"+ )++spec :: Spec+spec = do+ describe "runSecretSpec" $ do+ it "resolves a declared secret" $ withManifest $ \_ cfg -> do+ result <-+ runEff+ . runErrorNoCallStack @SecretsError+ . runSecretSpec cfg+ $ getSecret "MY_SECRET"++ result `shouldBe` Right "hunter2"++ it "errors on an undeclared secret" $ withManifest $ \_ cfg -> do+ result <-+ runEff+ . runErrorNoCallStack @SecretsError+ . runSecretSpec cfg+ $ getSecret "NOPE"++ result `shouldBe` Left (SecretNotFound "NOPE")++ describe "getConfigValue" $ do+ it "resolves a Secret config value via secretspec" $ withManifest $ \_ cfg -> do+ p <- newMVar (SecretConfig (Secret "MY_SECRET"))+ result <-+ runEff+ . runErrorNoCallStack @SecretsError+ . runSecretSpec cfg+ . runLog silentLog+ . runReader p+ $ getConfigValue secret_val++ result `shouldBe` Right "hunter2"
+ test/Spec.hs view
@@ -0,0 +1,2 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}+{-# OPTIONS_GHC -Wno-missing-export-lists #-}