unleash-client-haskell (empty) → 0.4.7
raw patch · 6 files changed
+632/−0 lines, 6 filesdep +aesondep +basedep +containers
Dependencies added: aeson, base, containers, http-client-tls, http-media, mtl, record-dot-preprocessor, record-hasfield, servant, servant-client, text, time, unleash-client-haskell, unleash-client-haskell-core, unliftio
Files
- CHANGELOG.md +116/−0
- README.md +41/−0
- example/Main.hs +92/−0
- src/Unleash/Client.hs +205/−0
- src/Unleash/Internal/HttpClient.hs +95/−0
- unleash-client-haskell.cabal +83/−0
+ CHANGELOG.md view
@@ -0,0 +1,116 @@+## <small>0.4.7 (2023-09-14)</small>++* Add CHANGELOG.md ([8bfd48b](https://github.com/finn-no/unleash-client-haskell/commit/8bfd48b))+* Don't do -O2 ([eb8850b](https://github.com/finn-no/unleash-client-haskell/commit/eb8850b))+* Set version to 0.4.7 ([20b23d9](https://github.com/finn-no/unleash-client-haskell/commit/20b23d9))+* unleash-client-haskell-core: 0.8.9 ([997d8e3](https://github.com/finn-no/unleash-client-haskell/commit/997d8e3))++++## <small>0.4.6 (2023-09-14)</small>++* Downgrade Cabal to 3.0 ([cf24d8b](https://github.com/finn-no/unleash-client-haskell/commit/cf24d8b))+* Set version to 0.4.6 ([8c813a9](https://github.com/finn-no/unleash-client-haskell/commit/8c813a9))+* unleash-client-haskell-core: 0.8.7 ([4ae98c3](https://github.com/finn-no/unleash-client-haskell/commit/4ae98c3))+* Version bound all deps ([d518273](https://github.com/finn-no/unleash-client-haskell/commit/d518273))++++## <small>0.4.5 (2023-09-13)</small>++* Add some more package properties ([5042c05](https://github.com/finn-no/unleash-client-haskell/commit/5042c05))+* Set version to 0.4.5 ([4cf0364](https://github.com/finn-no/unleash-client-haskell/commit/4cf0364))+* unleash-client-haskell-core: 0.8.6 ([33571e5](https://github.com/finn-no/unleash-client-haskell/commit/33571e5))++++## <small>0.4.4 (2023-02-27)</small>++* Set version to 0.4.4 ([a064750](https://github.com/finn-no/unleash-client-haskell/commit/a064750))++++## <small>0.4.3 (2023-02-27)</small>++* Set version to 0.8.5 ([4d4aec2](https://github.com/finn-no/unleash-client-haskell/commit/4d4aec2))+* unleash-client-haskell-core: 0.8.5 ([b82fffa](https://github.com/finn-no/unleash-client-haskell/commit/b82fffa))++++## <small>0.4.2 (2023-02-27)</small>++* Build with -haddock ([8433559](https://github.com/finn-no/unleash-client-haskell/commit/8433559))+* Set version to 0.4.2 ([822623a](https://github.com/finn-no/unleash-client-haskell/commit/822623a))++++## <small>0.4.1 (2023-02-06)</small>++* Add Haddock docs ([f909f98](https://github.com/finn-no/unleash-client-haskell/commit/f909f98))+* Set version to 0.4.1 ([8bbfd00](https://github.com/finn-no/unleash-client-haskell/commit/8bbfd00))+* Update README.md ([4121c0e](https://github.com/finn-no/unleash-client-haskell/commit/4121c0e))+* Update README.md ([d68a358](https://github.com/finn-no/unleash-client-haskell/commit/d68a358))+* Update README.md ([cf91866](https://github.com/finn-no/unleash-client-haskell/commit/cf91866))+* Update README.md ([1766f05](https://github.com/finn-no/unleash-client-haskell/commit/1766f05))+* Update unleash-haskell-client-core to 0.8.2 ([1c73d01](https://github.com/finn-no/unleash-client-haskell/commit/1c73d01))+* Update unleash-haskell-client-core to 0.8.4 ([39904a0](https://github.com/finn-no/unleash-client-haskell/commit/39904a0))++++## 0.4.0 (2023-01-27)++* Re-export some constructors from unleash-client-haskell-core ([dd0d7f5](https://github.com/finn-no/unleash-client-haskell/commit/dd0d7f5))+* Set to version 0.4.0 ([aef1428](https://github.com/finn-no/unleash-client-haskell/commit/aef1428))+* Update README.md ([2c94f80](https://github.com/finn-no/unleash-client-haskell/commit/2c94f80))+* Update README.md ([77a8c4c](https://github.com/finn-no/unleash-client-haskell/commit/77a8c4c))+* Update README.md ([9e9efbd](https://github.com/finn-no/unleash-client-haskell/commit/9e9efbd))++++## <small>0.3.1 (2023-01-26)</small>++* Move HttpClient from unleash-client-haskell-core ([7d85ac5](https://github.com/finn-no/unleash-client-haskell/commit/7d85ac5))+* Refactor ([2a34192](https://github.com/finn-no/unleash-client-haskell/commit/2a34192))+* Refactor HttpClient ([fbf5957](https://github.com/finn-no/unleash-client-haskell/commit/fbf5957))+* Set version to 0.3.1 ([319428c](https://github.com/finn-no/unleash-client-haskell/commit/319428c))+* Support TLS by default ([f596f51](https://github.com/finn-no/unleash-client-haskell/commit/f596f51))+* Update README.md ([8dcc937](https://github.com/finn-no/unleash-client-haskell/commit/8dcc937))+* Update README.md ([0e2d998](https://github.com/finn-no/unleash-client-haskell/commit/0e2d998))++++## 0.3.0 (2023-01-26)++* Set to version 0.3.0 ([24dcdfd](https://github.com/finn-no/unleash-client-haskell/commit/24dcdfd))+* Use MonadReader ([4a4e8d6](https://github.com/finn-no/unleash-client-haskell/commit/4a4e8d6))++++## 0.2.0 (2023-01-25)++* Add apiKey to Config ([32d43c3](https://github.com/finn-no/unleash-client-haskell/commit/32d43c3))+* Set to version 0.2.0 ([e225ea6](https://github.com/finn-no/unleash-client-haskell/commit/e225ea6))+* Example: Die if registration fails ([8e5e021](https://github.com/finn-no/unleash-client-haskell/commit/8e5e021))++++## <small>0.1.1 (2023-01-25)</small>++* Set to version 0.1.1 ([6ec7b50](https://github.com/finn-no/unleash-client-haskell/commit/6ec7b50))+* Use unleash-client-haskell-core 0.7.0 ([6071600](https://github.com/finn-no/unleash-client-haskell/commit/6071600))++++## 0.1.0 (2023-01-25)++* Add variant getters ([ffddec4](https://github.com/finn-no/unleash-client-haskell/commit/ffddec4))+* Initial commit ([32df140](https://github.com/finn-no/unleash-client-haskell/commit/32df140))+* Misc. cleanup ([67eaac3](https://github.com/finn-no/unleash-client-haskell/commit/67eaac3))+* Update and add comments ([255096d](https://github.com/finn-no/unleash-client-haskell/commit/255096d))+* Update comments ([f31eda2](https://github.com/finn-no/unleash-client-haskell/commit/f31eda2))+* Update README.md ([a39d5a2](https://github.com/finn-no/unleash-client-haskell/commit/a39d5a2))+* Update README.md ([2d0601c](https://github.com/finn-no/unleash-client-haskell/commit/2d0601c))+* Update README.md ([0885486](https://github.com/finn-no/unleash-client-haskell/commit/0885486))+* Update unleash-client-haskell-core ([4f1f503](https://github.com/finn-no/unleash-client-haskell/commit/4f1f503))+* Use record-dot-preprocessor ([fd68143](https://github.com/finn-no/unleash-client-haskell/commit/fd68143))+* Use unleash-client-haskell-core at 0.6.5 tag ([a8d2040](https://github.com/finn-no/unleash-client-haskell/commit/a8d2040))
+ README.md view
@@ -0,0 +1,41 @@+# unleash-client-haskell++This is an [Unleash](https://www.getunleash.io/) client SDK for Haskell.++`Unleash.Client` provides a stateful HTTP client with functions and types for:++- Client configuration+- Client registration+- Fetching feature toggles+- Fetching variants+- Sending metrics++You'll need to spawn threads and handle errors yourself. This is demonstrated in the [example application](example/Main.hs).++`unleash-client-haskell` is a (working) work in progress and the API is likely to change.++See [unleash-client-haskell-core](https://github.com/finn-no/unleash-client-haskell-core) for a bare-bones feature toggle evaluation library.++## Build++```+nix-build+```++## Maintainers++- [Even Brenden](mailto:uch@anythingexternal.com)+- [Eirik Meland](mailto:eirik.meland@gmail.com)++## Dependencies++- aeson (BSD-3-Clause)+- containers (BSD-3-Clause)+- http-client-tls (MIT)+- http-media (MIT)+- servant (BSD-3-Clause)+- servant-client (BSD-3-Clause)+- text (BSD-2-Clause)+- time (BSD-2-Clause)+- unleash-client-haskell-core (MIT)+- unliftio (MIT)
+ example/Main.hs view
@@ -0,0 +1,92 @@+{- |+Module : Main+Copyright : Copyright © FINN.no AS, Inc. All rights reserved.+License : MIT+Stability : experimental++Example application that uses unleash-client-haskell. Spawns a state poller thread that updates the feature toggles, a metrics sender thread, and an application that continuously reads a feature toggle. The application will block until the first feature toggle set is received.+-}+module Main where++import Control.Concurrent (threadDelay)+import Control.Monad (forever)+import Control.Monad.Reader (ReaderT (runReaderT))+import Control.Monad.Reader.Class (asks)+import Data.Foldable (traverse_)+import Data.Text (Text)+import qualified Data.Text as T+import Data.Void (Void)+import Servant.Client (BaseUrl (BaseUrl), Scheme (Http))+import System.Exit (die)+import Unleash (emptyContext)+import Unleash.Client (+ HasUnleash (..),+ UnleashConfig (..),+ isEnabled,+ makeUnleashConfig,+ pollToggles,+ pushMetrics,+ registerClient,+ )+import UnliftIO++unleashServer :: BaseUrl+unleashServer = BaseUrl Http "your-unleash-server" 80 mempty++secretKey :: Maybe Text+secretKey = Nothing++featureToggle :: Text+featureToggle = "your-feature-toggle"++type Program a = ReaderT AppConfig IO a++data AppConfig = AppConfig {unleashConfig :: UnleashConfig}++instance HasUnleash AppConfig where+ getUnleashConfig = unleashConfig++main :: IO ()+main = do+ config <- makeUnleashConfig "unleash-client-haskell-example" "localhost" unleashServer secretKey+ runReaderT program (AppConfig config)++program :: Program ()+program = do+ registerApplication+ let threads = [statePoller, metricsPusher, application]+ runConcurrently $ traverse_ Concurrently threads++application :: Program Void+application =+ forever do+ enabled <- isEnabled featureToggle emptyContext+ liftIO . putStrLn $ T.unpack featureToggle <> " is " <> (if enabled then "enabled" else "disabled")+ liftIO . threadDelay $ 2 * 1000 * 1000++registerApplication :: Program ()+registerApplication = do+ registerClient+ >>= liftIO . \case+ Left error -> die $ "Could not register application (" <> show error <> ")"+ Right _ -> putStrLn "Application registered"++statePoller :: Program Void+statePoller = do+ config <- asks getUnleashConfig+ forever do+ pollToggles+ >>= liftIO . \case+ Left error -> putStrLn $ "Could not get state (" <> show error <> ")"+ Right _ -> putStrLn "State received"+ liftIO . threadDelay $ config.statePollIntervalInSeconds * 1000 * 1000++metricsPusher :: Program Void+metricsPusher = do+ config <- asks getUnleashConfig+ forever do+ liftIO . threadDelay $ config.metricsPushIntervalInSeconds * 1000 * 1000+ pushMetrics+ >>= liftIO . \case+ Left error -> putStrLn $ "Could not send metrics (" <> show error <> ")"+ Right _ -> putStrLn "Metrics sent"
+ src/Unleash/Client.hs view
@@ -0,0 +1,205 @@+{- |+Module : Unleash.Client+Copyright : Copyright © FINN.no AS, Inc. All rights reserved.+License : MIT+Stability : experimental++Functions and types that constitute an Unleash client SDK.++This module re-exports select constructors from [unleash-client-haskell-core](https://github.com/finn-no/unleash-client-haskell-core).+-}+module Unleash.Client (+ makeUnleashConfig,+ UnleashConfig (..),+ HasUnleash (..),+ registerClient,+ pollToggles,+ pushMetrics,+ isEnabled,+ tryIsEnabled,+ getVariant,+ tryGetVariant,+ -- Re-exports+ Context (..),+ emptyContext,+ VariantResponse (..),+) where++import Control.Concurrent.MVar+import Control.Monad (unless, void)+import Control.Monad.IO.Class (MonadIO (liftIO))+import Control.Monad.Reader (MonadReader, asks)+import Data.Text (Text)+import Data.Time (UTCTime, getCurrentTime)+import Network.HTTP.Client.TLS (newTlsManager)+import Servant.Client (BaseUrl, ClientEnv, ClientError, mkClientEnv)+import Unleash (Context (..), Features, MetricsPayload (..), RegisterPayload (..), VariantResponse (..), emptyContext, emptyVariantResponse, featureGetVariant, featureIsEnabled)+import Unleash.Internal.HttpClient (getAllClientFeatures, register, sendMetrics)++-- | Smart constructor for Unleash client configuration. Initializes the mutable variables properly.+makeUnleashConfig ::+ MonadIO m =>+ -- | Application name.+ Text ->+ -- | Instance identifier.+ Text ->+ -- | Unleash server base URL.+ BaseUrl ->+ -- | API key for authorization.+ Maybe Text ->+ -- | Configuration instance.+ m UnleashConfig+makeUnleashConfig applicationName instanceId serverUrl apiKey = do+ state <- liftIO newEmptyMVar+ metrics <- liftIO $ newMVar mempty+ metricsBucketStart <- liftIO $ getCurrentTime >>= newMVar+ manager <- newTlsManager+ let clientEnv = mkClientEnv manager serverUrl+ pure $+ UnleashConfig+ { applicationName = applicationName,+ instanceId = instanceId,+ state = state,+ statePollIntervalInSeconds = 4,+ metrics = metrics,+ metricsBucketStart = metricsBucketStart,+ metricsPushIntervalInSeconds = 8,+ apiKey = apiKey,+ httpClientEnvironment = clientEnv+ }++-- | Unleash client configuration. Use the smart constructor or make sure the mutable metrics variables are not empty.+data UnleashConfig = UnleashConfig+ { -- | Application name.+ applicationName :: Text,+ -- | Instance identifier.+ instanceId :: Text,+ -- | Full client feature set state.+ state :: MVar Features,+ -- | Feature set state update interval.+ statePollIntervalInSeconds :: Int,+ -- | Collected metrics state.+ metrics :: MVar [(Text, Bool)],+ -- | Current metrics bucket start time.+ metricsBucketStart :: MVar UTCTime,+ -- | Metrics sending interval.+ metricsPushIntervalInSeconds :: Int,+ -- | API key for authorization.+ apiKey :: Maybe Text,+ -- | HTTP client environment.+ httpClientEnvironment :: ClientEnv+ }++-- | Reader monad convenience class. Use this to get an Unleash configuration from inside of an application configuration (for example).+class HasUnleash r where+ getUnleashConfig :: r -> UnleashConfig++-- | Register client for the Unleash server. Call this on application startup before calling the state poller and metrics pusher functions.+registerClient :: (HasUnleash r, MonadReader r m, MonadIO m) => m (Either ClientError ())+registerClient = do+ config <- asks getUnleashConfig+ now <- liftIO getCurrentTime+ let registrationPayload :: RegisterPayload+ registrationPayload =+ RegisterPayload+ { appName = config.applicationName,+ instanceId = config.instanceId,+ started = now,+ intervalSeconds = config.metricsPushIntervalInSeconds+ }+ void <$> register config.httpClientEnvironment config.apiKey registrationPayload++-- | Fetch the most recent feature toggle set from the Unleash server. Meant to be run every statePollIntervalInSeconds. Non-blocking.+pollToggles :: (HasUnleash r, MonadReader r m, MonadIO m) => m (Either ClientError ())+pollToggles = do+ config <- asks getUnleashConfig+ eitherFeatures <- getAllClientFeatures config.httpClientEnvironment config.apiKey+ either (const $ pure ()) (updateState config.state) eitherFeatures+ pure . void $ eitherFeatures+ where+ updateState state value = do+ isUpdated <- liftIO $ tryPutMVar state value+ liftIO . unless isUpdated . void $ swapMVar state value++-- | Push metrics to the Unleash server. Meant to be run every metricsPushIntervalInSeconds. Blocks if the mutable metrics variables are empty.+pushMetrics :: (HasUnleash r, MonadReader r m, MonadIO m) => m (Either ClientError ())+pushMetrics = do+ config <- asks getUnleashConfig+ now <- liftIO getCurrentTime+ lastBucketStart <- liftIO $ swapMVar config.metricsBucketStart now+ bucket <- liftIO $ swapMVar config.metrics mempty+ let metricsPayload =+ MetricsPayload+ { appName = config.applicationName,+ instanceId = config.instanceId,+ start = lastBucketStart,+ stop = now,+ toggles = bucket+ }+ void <$> sendMetrics config.httpClientEnvironment config.apiKey metricsPayload++-- | Check if a feature is enabled or not. Blocks until first feature toggle set is received. Blocks if the mutable metrics variables are empty.+isEnabled ::+ (HasUnleash r, MonadReader r m, MonadIO m) =>+ -- | Feature toggle name.+ Text ->+ -- | Client context.+ Context ->+ -- | Whether or not the feature toggle is enabled.+ m Bool+isEnabled feature context = do+ config <- asks getUnleashConfig+ state <- liftIO . readMVar $ config.state+ enabled <- featureIsEnabled state feature context+ liftIO $ modifyMVar_ config.metrics (\info -> pure $ (feature, enabled) : info)+ pure enabled++-- | Check if a feature is enabled or not. Returns false for all toggles until first toggle set is received. Blocks if the mutable metrics variables are empty.+tryIsEnabled ::+ (HasUnleash r, MonadReader r m, MonadIO m) =>+ -- | Feature toggle name.+ Text ->+ -- | Client context.+ Context ->+ -- | Whether or not the feature toggle is enabled.+ m Bool+tryIsEnabled feature context = do+ config <- asks getUnleashConfig+ maybeState <- liftIO . tryReadMVar $ config.state+ case maybeState of+ Just state -> do+ enabled <- featureIsEnabled state feature context+ liftIO $ modifyMVar_ config.metrics (\info -> pure $ (feature, enabled) : info)+ pure enabled+ Nothing -> pure False++-- | Get a variant. Blocks until first feature toggle set is received.+getVariant ::+ (HasUnleash r, MonadReader r m, MonadIO m) =>+ -- | Feature toggle name.+ Text ->+ -- | Client context.+ Context ->+ -- | Variant.+ m VariantResponse+getVariant feature context = do+ config <- asks getUnleashConfig+ state <- liftIO . readMVar $ config.state+ featureGetVariant state feature context++-- | Get a variant. Returns an empty variant until first toggle set is received.+tryGetVariant ::+ (HasUnleash r, MonadReader r m, MonadIO m) =>+ -- | Feature toggle name.+ Text ->+ -- | Client context.+ Context ->+ -- | Variant.+ m VariantResponse+tryGetVariant feature context = do+ config <- asks getUnleashConfig+ maybeState <- liftIO . tryReadMVar $ config.state+ case maybeState of+ Just state -> do+ featureGetVariant state feature context+ Nothing -> pure emptyVariantResponse
+ src/Unleash/Internal/HttpClient.hs view
@@ -0,0 +1,95 @@+{- |+Module : Unleash.Internal.HttpClient+Copyright : Copyright © FINN.no AS, Inc. All rights reserved.+License : MIT+Stability : experimental+-}+module Unleash.Internal.HttpClient (+ getAllClientFeatures,+ register,+ sendMetrics,+) where++import Control.Monad.IO.Class (MonadIO, liftIO)+import Data.Aeson (ToJSON, encode)+import qualified Data.List.NonEmpty as NE+import Data.Map.Strict (Map, fromListWith)+import Data.Proxy (Proxy (..))+import Data.Text (Text)+import qualified Data.Text as T+import Data.Version (showVersion)+import qualified Network.HTTP.Media as M+import Paths_unleash_client_haskell (version)+import Servant.API (Accept (contentTypes), Get, Header, JSON, MimeRender (mimeRender), NoContent, PostNoContent, ReqBody, type (:<|>) (..), type (:>))+import Servant.Client (ClientEnv, ClientError, client, runClientM)+import Unleash.Internal.DomainTypes (Features, fromJsonFeatures, supportedStrategies)+import Unleash.Internal.JsonTypes (FullMetricsBucket (..), FullMetricsPayload (..), FullRegisterPayload (..), MetricsPayload, RegisterPayload, YesAndNoes (..))+import qualified Unleash.Internal.JsonTypes as UJT++type Register = "api" :> "client" :> "register" :> Header "Authorization" Text :> Header "Content-Type" Text :> ReqBody '[CustomJSON] FullRegisterPayload :> PostNoContent+type GetAllClientFeatures = "api" :> "client" :> "features" :> Header "Authorization" Text :> Get '[JSON] UJT.Features+type SendMetrics = "api" :> "client" :> "metrics" :> Header "Authorization" Text :> ReqBody '[CustomJSON] FullMetricsPayload :> PostNoContent+type Api = GetAllClientFeatures :<|> SendMetrics :<|> Register++getAllClientFeatures' :<|> sendMetrics' :<|> register' = client api++api :: Proxy Api+api = Proxy++type ApiKey = Text++data CustomJSON = CustomJSON++-- Remove charset=utf-8 because older versions of Unleash (e.g. 3.17.4) does not recognize it+instance Accept CustomJSON where+ contentTypes _ =+ "application" M.// "json"+ NE.:| ["application" M.// "json"]++instance {-# OVERLAPPABLE #-} ToJSON a => MimeRender CustomJSON a where+ mimeRender _ = encode++register :: MonadIO m => ClientEnv -> Maybe ApiKey -> RegisterPayload -> m (Either ClientError NoContent)+register clientEnv apiKey registerPayload = do+ let fullRegisterPayload =+ FullRegisterPayload+ { appName = registerPayload.appName,+ instanceId = registerPayload.instanceId,+ sdkVersion = "unleash-client-haskell:" <> (T.pack . showVersion) version,+ strategies = supportedStrategies,+ started = registerPayload.started,+ interval = registerPayload.intervalSeconds * 1000+ }+ liftIO $ runClientM (register' apiKey (Just "application/json") fullRegisterPayload) clientEnv++getAllClientFeatures :: MonadIO m => ClientEnv -> Maybe ApiKey -> m (Either ClientError Features)+getAllClientFeatures clientEnv apiKey = do+ eitherFeatures <- liftIO $ runClientM (getAllClientFeatures' apiKey) clientEnv+ pure $ fromJsonFeatures <$> eitherFeatures++sendMetrics :: MonadIO m => ClientEnv -> Maybe ApiKey -> MetricsPayload -> m (Either ClientError NoContent)+sendMetrics clientEnv apiKey metricsPayload = do+ liftIO $ runClientM (sendMetrics' apiKey fullMetricsPayload) clientEnv+ where+ fullMetricsPayload :: FullMetricsPayload+ fullMetricsPayload =+ FullMetricsPayload+ { appName = metricsPayload.appName,+ instanceId = metricsPayload.instanceId,+ bucket =+ FullMetricsBucket+ { start = metricsPayload.start,+ stop = metricsPayload.stop,+ toggles = makeMapOfYesAndNoes metricsPayload.toggles+ }+ }+ makeMapOfYesAndNoes :: [(Text, Bool)] -> Map Text YesAndNoes+ makeMapOfYesAndNoes tuples = do+ let withSingletonLists :: [(Text, [Bool])] = (\(k, v) -> (k, [v])) <$> tuples+ let asMap :: (Map Text [Bool]) = fromListWith (++) withSingletonLists+ boolsToYesAndNoes <$> asMap+ boolsToYesAndNoes :: [Bool] -> YesAndNoes+ boolsToYesAndNoes bools = do+ let yes = length $ filter id bools+ let no = length bools - yes+ YesAndNoes yes no
+ unleash-client-haskell.cabal view
@@ -0,0 +1,83 @@+cabal-version: 3.0++name: unleash-client-haskell+version: 0.4.7+synopsis: Unleash feature toggle client+description:+ This is an [Unleash](https://www.getunleash.io/) client SDK for Haskell.++ 'Unleash.Client' provides a stateful HTTP client with functions and types for:++ - Client configuration+ - Client registration+ - Fetching feature toggles+ - Fetching variants+ - Sending metrics++ See [the source repo](https://github.com/finn-no/unleash-client-haskell) for more information and example usage.+category: Web+author: FINN.no+homepage: https://github.com/finn-no/unleash-client-haskell+maintainer: See README.md+license: MIT+copyright: Copyright © FINN.no AS, Inc. All rights reserved.+extra-doc-files: CHANGELOG.md+extra-source-files: README.md++common all+ build-depends:+ base >=4.7 && <5,+ record-dot-preprocessor >= 0.2.15 && < 0.3,+ record-hasfield >= 1.0 && < 1.1+ default-extensions:+ BlockArguments+ DeriveFoldable+ DeriveFunctor+ DeriveTraversable+ DerivingStrategies+ LambdaCase+ OverloadedStrings+ RankNTypes+ ScopedTypeVariables+ ghc-options:+ -W+ -Wno-unused-top-binds+ -Wredundant-constraints+ -Wunused-packages+ -haddock+ -F -pgmF=record-dot-preprocessor+ default-language: Haskell2010++library+ import: all+ exposed-modules:+ Unleash.Client,+ other-modules:+ Unleash.Internal.HttpClient+ Paths_unleash_client_haskell+ autogen-modules:+ Paths_unleash_client_haskell+ hs-source-dirs: src+ build-depends:+ aeson >= 2.0.3 && < 2.1,+ containers >= 0.6.4 && < 0.7,+ http-client-tls >= 0.3.6 && < 0.4,+ http-media >= 0.8.0 && < 0.9,+ mtl >= 2.2.2 && < 2.3,+ servant >= 0.19.1 && < 0.20,+ servant-client >= 0.19 && < 0.20,+ text >= 1.2.5 && < 1.3,+ time >= 1.9.3 && < 1.10,+ unleash-client-haskell-core >= 0.8.9 && < 0.9++executable example+ import: all+ main-is: Main.hs+ hs-source-dirs: example+ build-depends:+ mtl,+ servant-client,+ text,+ unleash-client-haskell,+ unleash-client-haskell-core,+ unliftio >= 0.2.23 && < 0.3