Skip to content

Commit 92d8415

Browse files
authored
Merge pull request #154 from NoRedInk/redis-without-namespace
nri-redis: add handlerWithoutNamespace constructor
2 parents 430581a + 1adf200 commit 92d8415

19 files changed

Lines changed: 133 additions & 58 deletions

nri-redis/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# 0.4.1.0
2+
3+
- Add `handlerWithoutNamespace` for creating Redis handlers that skip
4+
namespace prefixing, for the cases where keys are shared with another
5+
system that doesn't follow the same namespacing convention.
6+
17
# 0.4.0.0
28

39
- Support GHC 9.10.2, GHC 9.12.2, `megaparsec-9.7.x`, `containers-0.7.x`

nri-redis/nri-redis.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ cabal-version: 1.18
55
-- see: https://github.com/sol/hpack
66

77
name: nri-redis
8-
version: 0.4.0.0
8+
version: 0.4.1.0
99
synopsis: An intuitive hedis wrapper library.
1010
description: Please see the README at <https://github.com/NoRedInk/haskell-libraries/tree/trunk/nri-redis#readme>.
1111
category: Web

nri-redis/package.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ synopsis: An intuitive hedis wrapper library.
33
description: Please see the README at <https://github.com/NoRedInk/haskell-libraries/tree/trunk/nri-redis#readme>.
44
homepage: https://github.com/NoRedInk/haskell-libraries/tree/trunk/nri-redis#readme
55
author: NoRedInk
6-
version: 0.4.0.0
6+
version: 0.4.1.0
77
maintainer: haskell-open-source@noredink.com
88
copyright: 2024 NoRedInk Corp.
99
github: NoRedInk/haskell-libraries/nri-redis

nri-redis/src/Redis.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module Redis
1010
( -- * Creating a redis handler
1111
Handler.handler,
1212
Handler.handlerAutoExtendExpire,
13+
Handler.handlerWithoutNamespace,
1314
Internal.Handler,
1415
Internal.HandlerAutoExtendExpire,
1516
Internal.Handler',

nri-redis/src/Redis/Handler.hs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
module Redis.Handler
55
( handler,
66
handlerAutoExtendExpire,
7+
handlerWithoutNamespace,
78
withQueryTimeoutMilliseconds,
89
withoutQueryTimeout,
910
)
@@ -31,15 +32,25 @@ import qualified Prelude
3132
-- | Produce a namespaced handler for Redis access.
3233
handler :: Text -> Settings.Settings -> Data.Acquire.Acquire Internal.Handler
3334
handler namespace settings = do
34-
(namespacedHandler, _) <- Data.Acquire.mkAcquire (acquireHandler namespace settings) releaseHandler
35+
(namespacedHandler, _) <- Data.Acquire.mkAcquire (acquireHandler (Just namespace) settings) releaseHandler
3536
namespacedHandler
3637
|> Prelude.pure
3738

39+
-- | Produce a Redis handler that does NOT prefix keys with a namespace.
40+
--
41+
-- Prefer 'handler' unless you have a specific reason to opt out of
42+
-- namespacing (e.g. reading/writing keys owned by another system). Using
43+
-- this constructor bypasses the namespace isolation that 'handler' enforces.
44+
handlerWithoutNamespace :: Settings.Settings -> Data.Acquire.Acquire Internal.Handler
45+
handlerWithoutNamespace settings = do
46+
(h, _) <- Data.Acquire.mkAcquire (acquireHandler Nothing settings) releaseHandler
47+
Prelude.pure h
48+
3849
-- | Produce a namespaced handler for Redis access.
3950
-- This will ensure that we extend all keys accessed by a query by a configured default time (see Settings.defaultExpiry)
4051
handlerAutoExtendExpire :: Text -> Settings.Settings -> Data.Acquire.Acquire Internal.HandlerAutoExtendExpire
4152
handlerAutoExtendExpire namespace settings = do
42-
(namespacedHandler, _) <- Data.Acquire.mkAcquire (acquireHandler namespace settings) releaseHandler
53+
(namespacedHandler, _) <- Data.Acquire.mkAcquire (acquireHandler (Just namespace) settings) releaseHandler
4354
namespacedHandler
4455
|> ( \handler' -> case Settings.defaultExpiry settings of
4556
Settings.NoDefaultExpiry ->
@@ -95,7 +106,7 @@ defaultExpiryKeysAfterSeconds secs handler' =
95106
Stack.withFrozenCallStack (Internal.doEval handler' queryTimeout script')
96107
}
97108

98-
acquireHandler :: Text -> Settings.Settings -> IO (Internal.Handler' x, Connection)
109+
acquireHandler :: Maybe Text -> Settings.Settings -> IO (Internal.Handler' x, Connection)
99110
acquireHandler namespace settings = do
100111
connection <- do
101112
let connectionInfo = Settings.connectionInfo settings

nri-redis/src/Redis/Internal.hs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ data Handler' (x :: HasAutoExtendExpire) = Handler'
237237
{ doQuery :: (Stack.HasCallStack) => forall a. Settings.QueryTimeout -> Query a -> Task Error a,
238238
doTransaction :: (Stack.HasCallStack) => forall a. Settings.QueryTimeout -> Query a -> Task Error a,
239239
doEval :: (Stack.HasCallStack) => forall a. (Database.Redis.RedisResult a) => Settings.QueryTimeout -> Script.Script a -> Task Error a,
240-
namespace :: Text,
240+
namespace :: Maybe Text,
241241
maxKeySize :: Settings.MaxKeySize,
242242
queryTimeout :: Settings.QueryTimeout
243243
}
@@ -260,7 +260,7 @@ type HandlerAutoExtendExpire = Handler' 'AutoExtendExpire
260260
-- to run them using 'transaction'
261261
query :: (Stack.HasCallStack) => Handler' x -> Query a -> Task Error a
262262
query handler query' =
263-
namespaceQuery (namespace handler ++ ":") query'
263+
namespaceQuery (namespacePrefix handler) query'
264264
|> Task.andThen (ensureMaxKeySize handler)
265265
|> Task.andThen (Stack.withFrozenCallStack (doQuery handler) (queryTimeout handler))
266266

@@ -272,15 +272,21 @@ query handler query' =
272272
-- see redis transaction semantics here: https://redis.io/topics/transactions
273273
transaction :: (Stack.HasCallStack) => Handler' x -> Query a -> Task Error a
274274
transaction handler query' =
275-
namespaceQuery (namespace handler ++ ":") query'
275+
namespaceQuery (namespacePrefix handler) query'
276276
|> Task.andThen (ensureMaxKeySize handler)
277277
|> Task.andThen (Stack.withFrozenCallStack (doTransaction handler) (queryTimeout handler))
278278

279279
eval :: (Stack.HasCallStack, Database.Redis.RedisResult a) => Handler' x -> Script.Script a -> Task Error a
280280
eval handler script =
281-
Script.mapKeys (\key -> Task.succeed (namespace handler ++ ":" ++ key)) script
281+
Script.mapKeys (\key -> Task.succeed (namespacePrefix handler ++ key)) script
282282
|> Task.andThen (Stack.withFrozenCallStack (doEval handler) (queryTimeout handler))
283283

284+
namespacePrefix :: Handler' x -> Text
285+
namespacePrefix handler =
286+
case namespace handler of
287+
Just ns -> ns ++ ":"
288+
Nothing -> ""
289+
284290
namespaceQuery :: Text -> Query a -> Task err (Query a)
285291
namespaceQuery prefix query' =
286292
mapKeys (\key -> Task.succeed (prefix ++ key)) query'

nri-redis/test/Helpers.hs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@ import qualified Prelude
1111

1212
data TestHandlers = TestHandlers
1313
{ autoExtendExpireHandler :: Redis.HandlerAutoExtendExpire,
14-
handler :: Redis.Handler
14+
handler :: Redis.Handler,
15+
noNamespaceHandler :: Redis.Handler
1516
}
1617

1718
getHandlers :: Conduit.Acquire TestHandlers
1819
getHandlers = do
1920
settings <- Conduit.liftIO (Environment.decode Settings.decoder)
2021
autoExtendExpireHandler <- Handler.handlerAutoExtendExpire "tests-auto-extend-expire" settings {Settings.defaultExpiry = Settings.ExpireKeysAfterSeconds 1}
2122
handler <- Handler.handler "tests" settings {Settings.defaultExpiry = Settings.NoDefaultExpiry}
22-
Prelude.pure TestHandlers {autoExtendExpireHandler, handler}
23+
noNamespaceHandler <- Handler.handlerWithoutNamespace settings {Settings.defaultExpiry = Settings.NoDefaultExpiry}
24+
Prelude.pure TestHandlers {autoExtendExpireHandler, handler, noNamespaceHandler}
2325

2426
-- | Historical context:
2527
-- Golden results are slightly different between GHC 9.2.x and 8.10.x due

nri-redis/test/Spec/Redis.hs

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,14 @@ spanForFailingTask task =
5757
Prelude.fail "Expected task to fail"
5858

5959
tests :: TestHandlers -> Test.Test
60-
tests TestHandlers {handler, autoExtendExpireHandler} =
60+
tests TestHandlers {handler, autoExtendExpireHandler, noNamespaceHandler} =
6161
Test.describe
6262
"Redis Library"
6363
[ Test.describe "query tests using handler" (queryTests handler),
6464
Test.describe "query tests using auto extend expire handler" (queryTests autoExtendExpireHandler),
6565
Test.describe "observability tests" (observabilityTests handler),
66-
Test.describe "ttl tests" (ttlTests handler autoExtendExpireHandler)
66+
Test.describe "ttl tests" (ttlTests handler autoExtendExpireHandler),
67+
Test.describe "no-namespace handler tests" (noNamespaceTests noNamespaceHandler handler)
6768
]
6869

6970
-- We want to test all of our potential makeApi alternatives because it's easy
@@ -486,9 +487,57 @@ ttlTests handler autoExtendExpireHandler =
486487
Expect.equal result Redis.TTLKeyNotFound
487488
]
488489

490+
-- | Tests that pin down the contract of `handlerWithoutNamespace`: keys are
491+
-- neither prefixed on the way in nor stripped on the way out. The
492+
-- `nsHandler` argument is the regular namespaced `handler` (namespace
493+
-- `"tests"`); we use it to confirm prefixing happens on the namespaced side
494+
-- but not on the no-namespace side.
495+
noNamespaceTests :: Redis.Handler -> Redis.Handler -> List Test.Test
496+
noNamespaceTests noNs nsHandler =
497+
[ Test.test "set via no-namespace handler stores the key under its literal name" <| \() -> do
498+
-- Write at literal key "noNs::literalKey" using the no-namespace handler.
499+
Redis.set api "noNs::literalKey" "value-no-ns" |> Redis.query noNs |> Expect.succeeds
500+
-- The namespaced handler would look at "tests:noNs::literalKey", which
501+
-- nothing has written to, so it should see Nothing.
502+
result <- Redis.get api "noNs::literalKey" |> Redis.query nsHandler |> Expect.succeeds
503+
Expect.equal result Nothing,
504+
Test.test "get via no-namespace handler reads the literal key (no prefix added)" <| \() -> do
505+
-- Namespaced handler writes "noNs::roundTrip" → redis sees "tests:noNs::roundTrip".
506+
Redis.set api "noNs::roundTrip" "via-namespace" |> Redis.query nsHandler |> Expect.succeeds
507+
-- The no-namespace handler can reach that same value by spelling out
508+
-- the full prefixed key, since it adds no prefix of its own.
509+
result <- Redis.get api "tests:noNs::roundTrip" |> Redis.query noNs |> Expect.succeeds
510+
Expect.equal result (Just "via-namespace"),
511+
Test.test "eval via no-namespace handler does not prefix script keys" <| \() -> do
512+
let script = [Redis.script|return ${Redis.Key "noNs::evalKey"}|]
513+
(result :: Text) <- Redis.eval noNs script |> Expect.succeeds
514+
Expect.equal result "noNs::evalKey",
515+
Test.test "foldWithScan via no-namespace handler returns keys verbatim" <| \() -> do
516+
let scanPrefix = "noNs::scanTest::"
517+
let firstKey = scanPrefix ++ "k1"
518+
let nonEmptyDict =
519+
NonEmptyDict.init firstKey "v1"
520+
<| Dict.fromList [(scanPrefix ++ "k2", "v2")]
521+
let expectedKeys =
522+
NonEmptyDict.toDict nonEmptyDict
523+
|> Dict.keys
524+
Redis.mset api nonEmptyDict |> Redis.query noNs |> Expect.succeeds
525+
let processBatch = \batchKeys acc ->
526+
Task.succeed (List.foldl Set.insert acc batchKeys)
527+
keySet <-
528+
Redis.foldWithScan noNs (Just (scanPrefix ++ "*")) (Just 10) processBatch Set.empty
529+
|> Expect.succeeds
530+
keySet
531+
|> Set.toList
532+
|> Expect.equal expectedKeys
533+
]
534+
489535
addNamespace :: Text -> Redis.Handler' x -> Redis.Handler' x
490536
addNamespace namespace handler' =
491-
handler' {Internal.namespace = Internal.namespace handler' ++ ":" ++ namespace}
537+
let combined = case Internal.namespace handler' of
538+
Just existing -> existing ++ ":" ++ namespace
539+
Nothing -> namespace
540+
in handler' {Internal.namespace = Just combined}
492541

493542
api :: Redis.Api Text Text
494543
api = Redis.textApi identity

nri-redis/test/golden-results-9.8/observability-spec-reporting-redis-counter-query

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ TracingSpan
66
Just
77
( "rootTracingSpanIO"
88
, SrcLoc
9-
{ srcLocPackage = "nri-redis-0.4.0.0-inplace-tests"
9+
{ srcLocPackage = "nri-redis-0.4.1.0-inplace-tests"
1010
, srcLocModule = "Spec.Redis"
1111
, srcLocFile = "test/Spec/Redis.hs"
1212
, srcLocStartLine = 31
@@ -29,12 +29,12 @@ TracingSpan
2929
Just
3030
( "query"
3131
, SrcLoc
32-
{ srcLocPackage = "nri-redis-0.4.0.0-inplace-tests"
32+
{ srcLocPackage = "nri-redis-0.4.1.0-inplace-tests"
3333
, srcLocModule = "Spec.Redis"
3434
, srcLocFile = "test/Spec/Redis.hs"
35-
, srcLocStartLine = 123
35+
, srcLocStartLine = 124
3636
, srcLocStartCol = 9
37-
, srcLocEndLine = 123
37+
, srcLocEndLine = 124
3838
, srcLocEndCol = 28
3939
}
4040
)

nri-redis/test/golden-results-9.8/observability-spec-reporting-redis-counter-transaction

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ TracingSpan
66
Just
77
( "rootTracingSpanIO"
88
, SrcLoc
9-
{ srcLocPackage = "nri-redis-0.4.0.0-inplace-tests"
9+
{ srcLocPackage = "nri-redis-0.4.1.0-inplace-tests"
1010
, srcLocModule = "Spec.Redis"
1111
, srcLocFile = "test/Spec/Redis.hs"
1212
, srcLocStartLine = 31
@@ -29,12 +29,12 @@ TracingSpan
2929
Just
3030
( "transaction"
3131
, SrcLoc
32-
{ srcLocPackage = "nri-redis-0.4.0.0-inplace-tests"
32+
{ srcLocPackage = "nri-redis-0.4.1.0-inplace-tests"
3333
, srcLocModule = "Spec.Redis"
3434
, srcLocFile = "test/Spec/Redis.hs"
35-
, srcLocStartLine = 130
35+
, srcLocStartLine = 131
3636
, srcLocStartCol = 9
37-
, srcLocEndLine = 130
37+
, srcLocEndLine = 131
3838
, srcLocEndCol = 34
3939
}
4040
)

0 commit comments

Comments
 (0)