launchdarkly-server-sdk-redis-hedis (empty) → 1.0.0
raw patch · 12 files changed
+554/−0 lines, 12 filesdep +HUnitdep +aesondep +basesetup-changed
Dependencies added: HUnit, aeson, base, bytestring, exceptions, generic-lens, hedis, launchdarkly-server-sdk, text
Files
- CHANGELOG.md +5/−0
- LICENSE +13/−0
- README.md +45/−0
- Setup.hs +2/−0
- launchdarkly-server-sdk-redis-hedis.cabal +88/−0
- src/LaunchDarkly/Server/Store/Redis.hs +9/−0
- src/LaunchDarkly/Server/Store/Redis/Internal.hs +143/−0
- test/Spec.hs +23/−0
- test/Spec/Get.hs +43/−0
- test/Spec/Initialization.hs +71/−0
- test/Spec/Upsert.hs +75/−0
- test/Util.hs +37/−0
+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Change log++All notable changes to the LaunchDarkly SDK for Haskell - Redis Integration will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org).++
+ LICENSE view
@@ -0,0 +1,13 @@+Copyright 2023 Catamorphic, Co.++Licensed under the Apache License, Version 2.0 (the "License");+you may not use this file except in compliance with the License.+You may obtain a copy of the License at++ http://www.apache.org/licenses/LICENSE-2.0++Unless required by applicable law or agreed to in writing, software+distributed under the License is distributed on an "AS IS" BASIS,+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+See the License for the specific language governing permissions and+limitations under the License.
+ README.md view
@@ -0,0 +1,45 @@+# LaunchDarkly Server SDK for Haskell - Redis Integration++[](https://hackage.haskell.org/package/launchdarkly-server-sdk-redis-hedis)+[](https://circleci.com/gh/launchdarkly/haskell-server-sdk-redis-hedis)+[](https://launchdarkly.github.io/haskell-server-sdk-redis-hedis)++## LaunchDarkly overview++[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today!++[](https://twitter.com/intent/follow?screen_name=launchdarkly)++## Supported Haskell versions++This version of the LaunchDarkly SDK has been tested with Haskell v4.++## Getting started++Download a release archive from the [GitHub Releases](https://github.com/launchdarkly/haskell-server-sdk-redis-hedis/releases) for use in your project. Refer to the [SDK Redis documentation](https://docs.launchdarkly.com/sdk/features/storing-data/redis#haskell) for complete instructions on getting started with using the SDK with Redis.++## Learn more++Check out our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/sdk/server-side/haskell) or our [code-generated API documentation](https://launchdarkly.github.io/haskell-server-sdk/).++## Testing++We run integration tests for all our SDKs using a centralized test harness. This approach gives us the ability to test for consistency across SDKs, as well as test networking behavior in a long-running application. These tests cover each method in the SDK, and verify that event sending, flag evaluation, stream reconnection, and other aspects of the SDK all behave correctly.++## Contributing++We encourage pull requests and other contributions from the community. Check out our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contribute to this SDK.++## About LaunchDarkly++* LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can:+ * Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases.+ * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?).+ * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file.+ * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline.+* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/docs) for a complete list.+* Explore LaunchDarkly+ * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information+ * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides+ * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation+ * [launchdarkly.com/blog](https://launchdarkly.com/blog/ "LaunchDarkly Blog Documentation") for the latest product updates
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ launchdarkly-server-sdk-redis-hedis.cabal view
@@ -0,0 +1,88 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.35.1.+--+-- see: https://github.com/sol/hpack++name: launchdarkly-server-sdk-redis-hedis+version: 1.0.0+synopsis: LaunchDarkly Server-Side SDK - Redis Integration+description: Redis integration for the LaunchDarkly SDK using hedis+category: Web+homepage: https://github.com/launchdarkly/haskell-server-sdk-redis-hedis#readme+bug-reports: https://github.com/launchdarkly/haskell-server-sdk-redis-hedis/issues+author: LaunchDarkly+maintainer: dev@launchdarkly.com+copyright: 2023 Catamorphic, Co+license: Apache-2.0+license-file: LICENSE+build-type: Simple+extra-source-files:+ README.md+ CHANGELOG.md+ LICENSE++source-repository head+ type: git+ location: https://github.com/launchdarkly/haskell-server-sdk-redis-hedis++library+ exposed-modules:+ LaunchDarkly.Server.Store.Redis+ other-modules:+ LaunchDarkly.Server.Store.Redis.Internal+ Paths_launchdarkly_server_sdk_redis_hedis+ hs-source-dirs:+ src+ default-extensions:+ DataKinds+ DeriveAnyClass+ LambdaCase+ OverloadedStrings+ ScopedTypeVariables+ TypeApplications+ ghc-options: -fwarn-unused-imports -Wall -Wno-name-shadowing+ build-depends:+ aeson >=1.4.7.1 && <1.6 || >=2.0.1.0 && <2.2+ , base >=4.13.0.0 && <5+ , bytestring >=0.10.10.1 && <0.12+ , exceptions >=0.10.4 && <0.11+ , generic-lens >=2.0.0.0 && <2.3+ , hedis >=0.12.15 && <0.16+ , launchdarkly-server-sdk >=4.0 && <5+ , text >=1.2.4.0 && <2.1+ default-language: Haskell2010++test-suite haskell-server-sdk-redis-hedis-test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ Spec.Get+ Spec.Initialization+ Spec.Upsert+ Util+ LaunchDarkly.Server.Store.Redis+ LaunchDarkly.Server.Store.Redis.Internal+ Paths_launchdarkly_server_sdk_redis_hedis+ hs-source-dirs:+ test+ src+ default-extensions:+ DataKinds+ DeriveAnyClass+ LambdaCase+ OverloadedStrings+ ScopedTypeVariables+ TypeApplications+ ghc-options: -rtsopts -threaded -with-rtsopts=-N -Wno-name-shadowing -fwarn-unused-imports -Wall+ build-depends:+ HUnit+ , aeson >=1.4.7.1 && <1.6 || >=2.0.1.0 && <2.2+ , base >=4.13.0.0 && <5+ , bytestring >=0.10.10.1 && <0.12+ , exceptions >=0.10.4 && <0.11+ , generic-lens >=2.0.0.0 && <2.3+ , hedis >=0.12.15 && <0.16+ , launchdarkly-server-sdk+ , text >=1.2.4.0 && <2.1+ default-language: Haskell2010
+ src/LaunchDarkly/Server/Store/Redis.hs view
@@ -0,0 +1,9 @@+-- | The public interface for the LaunchDarkly Haskell Redis integration+module LaunchDarkly.Server.Store.Redis+ ( RedisStoreConfig+ , makeRedisStoreConfig+ , redisConfigSetNamespace+ , makeRedisStore+ ) where++import LaunchDarkly.Server.Store.Redis.Internal
+ src/LaunchDarkly/Server/Store/Redis/Internal.hs view
@@ -0,0 +1,143 @@+module LaunchDarkly.Server.Store.Redis.Internal+ ( RedisStoreConfig+ , makeRedisStoreConfig+ , redisConfigSetNamespace+ , makeRedisStore+ ) where++import Control.Exception (throwIO)+import Control.Monad (forM_, void)+import Control.Monad.Catch (Exception, Handler (..), catches)+import Control.Monad.IO.Class (MonadIO, liftIO)+import Data.Bifunctor (first)+import Data.ByteString (ByteString)+import Data.Functor ((<&>))+import Data.Generics.Product (getField)+import Data.Maybe (isJust)+import Data.Text (Text)+import qualified Data.Text as T+import Data.Text.Encoding (decodeUtf8, encodeUtf8)+import Data.Typeable (Typeable)+import Database.Redis+ ( Connection+ , ConnectionLostException+ , Redis+ , Reply+ , TxResult (..)+ , del+ , get+ , hget+ , hgetall+ , hset+ , multiExec+ , runRedis+ , set+ , watch+ )++import LaunchDarkly.AesonCompat (KeyMap, fromList, mapValues, objectKeys, toList)+import LaunchDarkly.Server.Store (PersistentDataStore (..), SerializedItemDescriptor (..), StoreResult, byteStringToVersionedData, serializeWithPlaceholder)++-- | Opaque type used to configure the Redis store integration.+data RedisStoreConfig = RedisStoreConfig+ { namespace :: Text+ , connection :: Connection+ }++-- | Create a default config from a given connection pool.+makeRedisStoreConfig :: Connection -> RedisStoreConfig+makeRedisStoreConfig con =+ RedisStoreConfig+ { namespace = "launchdarkly"+ , connection = con+ }++-- |+-- Configure the Redis key prefix. All keys are prefixed by default before+-- being inserted into Redis. The default prefix is "launchdarkly".+redisConfigSetNamespace :: Text -> RedisStoreConfig -> RedisStoreConfig+redisConfigSetNamespace namespace' config = config {namespace = namespace'}++-- |+-- Construct a `PersistentDataStore` that can then be used during SDK+-- configuration.+makeRedisStore :: RedisStoreConfig -> IO PersistentDataStore+makeRedisStore config =+ pure+ PersistentDataStore+ { persistentDataStoreUpsertFeature = redisUpsert config+ , persistentDataStoreGetFeature = redisGetFeature config+ , persistentDataStoreInitialize = redisInitialize config+ , persistentDataStoreIsInitialized = redisIsInitialized config+ , persistentDataStoreAllFeatures = redisGetAll config+ }++newtype RedisError = RedisError Text deriving (Typeable, Show, Exception)++makeKey :: RedisStoreConfig -> Text -> ByteString+makeKey config key = encodeUtf8 $ T.concat [namespace config, ":", key]++exceptOnReply :: (MonadIO m) => Either Reply a -> m a+exceptOnReply = \case+ Left err -> liftIO $ throwIO $ RedisError $ T.pack $ show err+ Right x -> pure x++run :: RedisStoreConfig -> Redis a -> StoreResult a+run config action =+ catches+ (runRedis (connection config) action <&> pure)+ [ Handler $ \(e :: ConnectionLostException) -> pure $ Left $ T.pack $ show e+ , Handler $ \(RedisError err) -> pure $ Left err+ ]++createSerializedItemDescriptor :: ByteString -> SerializedItemDescriptor+createSerializedItemDescriptor byteString = SerializedItemDescriptor (Just byteString) 0 False++redisInitialize :: RedisStoreConfig -> KeyMap (KeyMap SerializedItemDescriptor) -> StoreResult ()+redisInitialize config values = run config $ do+ del (map (makeKey config) $ objectKeys values) >>= void . exceptOnReply+ forM_ (toList values) $ \(kind, features) -> forM_ (toList features) $ \(key, feature) ->+ hset (makeKey config kind) (encodeUtf8 key) (serializeWithPlaceholder feature) >>= void . exceptOnReply+ set (makeKey config "$inited") "" >>= void . exceptOnReply++redisUpsert :: RedisStoreConfig -> Text -> Text -> SerializedItemDescriptor -> StoreResult Bool+redisUpsert = redisUpsertInternal (pure ())++redisUpsertInternal :: IO () -> RedisStoreConfig -> Text -> Text -> SerializedItemDescriptor -> StoreResult Bool+redisUpsertInternal hook config kind key opaque = run config tryUpsert+ where+ tryUpsert =+ watch [space]+ >>= void . exceptOnReply+ >> hget space (encodeUtf8 key)+ >>= exceptOnReply+ >>= \x ->+ liftIO hook >> case x of+ Nothing -> doInsert+ (Just byteString) -> case byteStringToVersionedData byteString of+ Nothing -> pure True+ Just decodedVersion ->+ if getField @"version" decodedVersion >= getField @"version" opaque+ then pure False+ else doInsert+ space = makeKey config kind+ doInsert =+ multiExec (hset space (encodeUtf8 key) (serializeWithPlaceholder opaque)) >>= \case+ TxSuccess _ -> pure True+ TxError err -> liftIO $ throwIO $ RedisError $ T.pack $ show err+ TxAborted -> tryUpsert++redisGetFeature :: RedisStoreConfig -> Text -> Text -> StoreResult (Maybe SerializedItemDescriptor)+redisGetFeature config kind key =+ run config $+ hget (makeKey config kind) (encodeUtf8 key)+ >>= exceptOnReply+ >>= \result -> pure $ createSerializedItemDescriptor <$> result++redisIsInitialized :: RedisStoreConfig -> StoreResult Bool+redisIsInitialized config =+ run config $ (get (makeKey config "$inited") >>= exceptOnReply) <&> isJust++redisGetAll :: RedisStoreConfig -> Text -> StoreResult (KeyMap SerializedItemDescriptor)+redisGetAll config kind =+ run config $ (hgetall (makeKey config kind) >>= exceptOnReply) <&> (mapValues createSerializedItemDescriptor . fromList . map (first decodeUtf8))
+ test/Spec.hs view
@@ -0,0 +1,23 @@+{-# LANGUAGE RecordWildCards #-}++module Main where++import Control.Monad (when)+import Test.HUnit (Counts (..), Test (TestLabel, TestList), runTestTT)++import qualified Spec.Get+import qualified Spec.Initialization+import qualified Spec.Upsert++import System.Exit (ExitCode (ExitFailure), exitWith)++main :: IO ()+main = do+ Counts {..} <-+ runTestTT $+ TestList+ [ TestLabel "Get" Spec.Get.allTests+ , TestLabel "Initialization" Spec.Initialization.allTests+ , TestLabel "Upsert" Spec.Upsert.allTests+ ]+ when (errors + failures > 0) $ exitWith (ExitFailure 1)
+ test/Spec/Get.hs view
@@ -0,0 +1,43 @@+module Spec.Get (allTests) where++import Data.Maybe (fromJust)+import LaunchDarkly.AesonCompat (lookupKey, mapValues)+import LaunchDarkly.Server.Store (PersistentDataStore (..), SerializedItemDescriptor (..))+import Test.HUnit+import Util (defaultPrefix, initialData, initialFlag, makeDefaultRedisBackend)++testCanExistingItem :: Test+testCanExistingItem = TestCase $ do+ backend <- makeDefaultRedisBackend defaultPrefix+ _ <- persistentDataStoreInitialize backend initialData+ flag <- persistentDataStoreGetFeature backend "features" "first-flag"++ assertEqual "" (Right $ Just expectedFlag) flag+ where+ expectedFlag = initialFlag {version = 0, deleted = False}++testHandlesNonExistentData :: Test+testHandlesNonExistentData = TestCase $ do+ backend <- makeDefaultRedisBackend defaultPrefix+ _ <- persistentDataStoreInitialize backend initialData+ flag <- persistentDataStoreGetFeature backend "features" "does-not-exist"++ assertEqual "" (Right Nothing) flag++testAllFlagsReturnsEveryFlag :: Test+testAllFlagsReturnsEveryFlag = TestCase $ do+ backend <- makeDefaultRedisBackend defaultPrefix+ _ <- persistentDataStoreInitialize backend initialData+ flags <- persistentDataStoreAllFeatures backend "features"++ assertEqual "" (Right expectedFlags) flags+ where+ expectedFlags = mapValues (\flag -> flag {version = 0, deleted = False}) $ fromJust $ lookupKey "features" initialData++allTests :: Test+allTests =+ TestList+ [ testCanExistingItem+ , testHandlesNonExistentData+ , testAllFlagsReturnsEveryFlag+ ]
+ test/Spec/Initialization.hs view
@@ -0,0 +1,71 @@+module Spec.Initialization (allTests) where++import LaunchDarkly.Server.Store (PersistentDataStore (..))+import Test.HUnit+import Util (defaultPrefix, emptyData, initialData, makeDefaultRedisBackend)++testClientIsNotInitializedByDefault :: Test+testClientIsNotInitializedByDefault = TestCase $ do+ backend <- makeDefaultRedisBackend defaultPrefix+ isInitialized <- persistentDataStoreIsInitialized backend++ assertEqual "" (Right False) isInitialized++testClientCanBeMarkedAsInitialized :: Test+testClientCanBeMarkedAsInitialized = TestCase $ do+ backend <- makeDefaultRedisBackend defaultPrefix+ initResult <- persistentDataStoreInitialize backend initialData+ isInitialized <- persistentDataStoreIsInitialized backend++ assertEqual "" (Right ()) initResult+ assertEqual "" (Right True) isInitialized++testClientCanSeeInitializiationFromAnotherClient :: Test+testClientCanSeeInitializiationFromAnotherClient = TestCase $ do+ activeBackend <- makeDefaultRedisBackend defaultPrefix+ passiveBackend <- makeDefaultRedisBackend defaultPrefix+ _ <- persistentDataStoreInitialize activeBackend initialData+ isActiveBackendInitialized <- persistentDataStoreIsInitialized activeBackend+ isPassiveBackendInitialized <- persistentDataStoreIsInitialized passiveBackend++ assertEqual "" (Right True) isActiveBackendInitialized+ assertEqual "" (Right True) isPassiveBackendInitialized++testDifferentPrefixesAreIndependent :: Test+testDifferentPrefixesAreIndependent = TestCase $ do+ initializedBackend <- makeDefaultRedisBackend "initialized"+ uninitializedBackend <- makeDefaultRedisBackend "uninitialized"+ _ <- persistentDataStoreInitialize initializedBackend initialData+ isInitializedBackendInitialized <- persistentDataStoreIsInitialized initializedBackend+ isUninitializedBackendInitialized <- persistentDataStoreIsInitialized uninitializedBackend++ assertEqual "" (Right True) isInitializedBackendInitialized+ assertEqual "" (Right False) isUninitializedBackendInitialized++testInitializationReplacesAllPreviousData :: Test+testInitializationReplacesAllPreviousData = TestCase $ do+ backend <- makeDefaultRedisBackend defaultPrefix+ _ <- persistentDataStoreInitialize backend initialData+ _ <- persistentDataStoreIsInitialized backend+ flagResult <- persistentDataStoreGetFeature backend "features" "first-flag"+ segmentResult <- persistentDataStoreGetFeature backend "segments" "first-segment"++ case flagResult of Right (Just _) -> pure (); _ -> assertFailure "first-flag was not present"+ case segmentResult of Right (Just _) -> pure (); _ -> assertFailure "first-segment was not present"++ _ <- persistentDataStoreInitialize backend emptyData+ flagResult' <- persistentDataStoreGetFeature backend "features" "first-flag"+ segmentResult' <- persistentDataStoreGetFeature backend "segments" "first-segment"++ case flagResult' of Right Nothing -> pure (); _ -> assertFailure "first-flag was not present"+ case segmentResult' of Right Nothing -> pure (); _ -> assertFailure "first-segment was not present"++allTests :: Test+allTests =+ TestList+ [ testClientIsNotInitializedByDefault+ , testClientCanBeMarkedAsInitialized+ , testClientCanSeeInitializiationFromAnotherClient+ , testDifferentPrefixesAreIndependent+ , testInitializationReplacesAllPreviousData+ ]
+ test/Spec/Upsert.hs view
@@ -0,0 +1,75 @@+module Spec.Upsert (allTests) where++import Data.ByteString (isInfixOf)+import Data.ByteString.Char8 (pack)+import Data.Char (toLower)+import GHC.Natural (Natural)+import qualified LaunchDarkly.Server.Store as Store+import Test.HUnit+import Text.Printf+import Util (defaultPrefix, emptyData, makeDefaultRedisBackend)++createTestFlag :: Natural -> Bool -> Store.SerializedItemDescriptor+createTestFlag version deleted =+ Store.SerializedItemDescriptor+ { Store.item = Just $ pack $ printf "{\"deleted\":%s,\"version\":%d}" (map toLower $ show deleted) version+ , Store.version = version+ , Store.deleted = deleted+ }++testHandlesVersionsAppropriately :: Natural -> Natural -> Natural -> Test+testHandlesVersionsAppropriately initialVersion secondVersion expectedVersion = TestCase $ do+ backend <- makeDefaultRedisBackend defaultPrefix+ _ <- Store.persistentDataStoreInitialize backend emptyData+ _ <- Store.persistentDataStoreUpsertFeature backend "features" "first-flag" $ createTestFlag initialVersion False+ _ <- Store.persistentDataStoreUpsertFeature backend "features" "first-flag" $ createTestFlag secondVersion False+ flag <- Store.persistentDataStoreGetFeature backend "features" "first-flag"++ assertEqual "" (Right $ Just expectedFlag) flag+ where+ expectedFlag = (createTestFlag expectedVersion False) {Store.version = 0, Store.deleted = False}++testUpsertionAfterDeletionHandlesVersionsAppropriately :: Natural -> Natural -> Natural -> Bool -> Test+testUpsertionAfterDeletionHandlesVersionsAppropriately initialVersion secondVersion expectedVersion expectedDeletionStatus = TestCase $ do+ backend <- makeDefaultRedisBackend defaultPrefix+ _ <- Store.persistentDataStoreInitialize backend emptyData+ _ <- Store.persistentDataStoreUpsertFeature backend "features" "first-flag" $ createTestFlag initialVersion True+ _ <- Store.persistentDataStoreUpsertFeature backend "features" "first-flag" $ createTestFlag secondVersion False+ flag <- Store.persistentDataStoreGetFeature backend "features" "first-flag"++ case flag of+ Right (Just Store.SerializedItemDescriptor {Store.item = Just item, Store.version = 0, Store.deleted = False}) -> do+ assertBool "" (expectedVersionString `isInfixOf` item)+ assertBool "" (expectedDeletionString `isInfixOf` item)+ _ -> assertFailure "failed to restore deleted file"+ where+ expectedVersionString = pack $ printf "\"version\":%d" expectedVersion+ expectedDeletionString = pack $ printf "\"deleted\":%s" (map toLower $ show expectedDeletionStatus)++testUpsertionToADeletionHandlesVersionsAppropriately :: Natural -> Natural -> Natural -> Bool -> Test+testUpsertionToADeletionHandlesVersionsAppropriately initialVersion secondVersion expectedVersion expectedDeletionStatus = TestCase $ do+ backend <- makeDefaultRedisBackend defaultPrefix+ _ <- Store.persistentDataStoreInitialize backend emptyData+ _ <- Store.persistentDataStoreUpsertFeature backend "features" "first-flag" $ createTestFlag initialVersion False+ _ <- Store.persistentDataStoreUpsertFeature backend "features" "first-flag" $ createTestFlag secondVersion True+ flag <- Store.persistentDataStoreGetFeature backend "features" "first-flag"++ case flag of+ Right (Just Store.SerializedItemDescriptor {Store.item = Just item, Store.version = 0, Store.deleted = False}) -> do+ assertBool "" (expectedVersionString `isInfixOf` item)+ assertBool "" (expectedDeletionString `isInfixOf` item)+ _ -> assertFailure "failed to restore deleted file"+ where+ expectedVersionString = pack $ printf "\"version\":%d" expectedVersion+ expectedDeletionString = pack $ printf "\"deleted\":%s" (map toLower $ show expectedDeletionStatus)++allTests :: Test+allTests =+ TestList+ [ TestLabel "Newer versions replace older" $ testHandlesVersionsAppropriately 1 2 2+ , TestLabel "Older versions do not replace newer" $ testHandlesVersionsAppropriately 2 1 2+ , TestLabel "Newer versions replace older" $ testUpsertionAfterDeletionHandlesVersionsAppropriately 1 2 2 False+ , TestLabel "Older versions do not replace newer" $ testUpsertionAfterDeletionHandlesVersionsAppropriately 2 1 2 True+ , TestLabel "Newer versions replace older" $ testUpsertionToADeletionHandlesVersionsAppropriately 1 2 2 True+ , TestLabel "Older versions do not replace newer" $ testUpsertionToADeletionHandlesVersionsAppropriately 2 1 2 False+ ]
+ test/Util.hs view
@@ -0,0 +1,37 @@+module Util where++import Control.Monad (void)+import Data.Function ((&))+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import qualified Database.Redis as R+import LaunchDarkly.AesonCompat (KeyMap, emptyObject, insertKey, singleton)+import LaunchDarkly.Server.Store (PersistentDataStore (..), SerializedItemDescriptor (..))+import LaunchDarkly.Server.Store.Redis (makeRedisStore, makeRedisStoreConfig, redisConfigSetNamespace)++defaultPrefix :: T.Text+defaultPrefix = "launchdarkly-test-prefix"++emptyData :: KeyMap (KeyMap SerializedItemDescriptor)+emptyData =+ emptyObject+ & insertKey "features" emptyObject+ & insertKey "segments" emptyObject++initialFlag :: SerializedItemDescriptor+initialFlag = SerializedItemDescriptor {version = 1, deleted = False, item = Just $ T.encodeUtf8 "flag"}++initialSegment :: SerializedItemDescriptor+initialSegment = SerializedItemDescriptor {version = 1, deleted = False, item = Just $ T.encodeUtf8 "segment"}++initialData :: KeyMap (KeyMap SerializedItemDescriptor)+initialData =+ emptyObject+ & insertKey "features" (singleton "first-flag" initialFlag)+ & insertKey "segments" (singleton "first-segment" initialSegment)++makeDefaultRedisBackend :: T.Text -> IO PersistentDataStore+makeDefaultRedisBackend prefix = do+ con <- R.checkedConnect R.defaultConnectInfo+ void $ R.runRedis con R.flushall+ makeRedisStore $ redisConfigSetNamespace prefix $ makeRedisStoreConfig con