From d530ae7d5fbb83b77ae7d6918ba812f2fee06104 Mon Sep 17 00:00:00 2001 From: Julien Jean Paul Sirocchi Date: Sat, 10 Aug 2019 12:18:36 +0100 Subject: [PATCH] Partial fix for #19 (#75) * - migrate cmd * - add contramap to Show, reformat implicitNotFound msg * - RESPParamWrite reformat implicitNotFound msg, reorg instances classes * - LowPriorityShowInstances -> ShowInstances and make sealed an package private * - ops revert to using traits for init order * - Read: modify implicitNotFound msg, replaced zip (unused and unneeded) with orElse (a bit more useful), combinators leverage Read constructors * - echo and ping now accept any A: Show, also require NonNullBulkString ==> A evidence * - reworded implicitNotFound msg, use widenLeft where appropriate * more fixes * - remove unneeded lazy vals in build.sbt for cross build components - add comment on why we shadow scala.js plugin * - toShowSyntax -> toShowOps * - address codacy issues * fixed compilation issue w/ scalatest 3.0.6 * wip geo * Ignoring metals+bloom directories * refactor Read instances names and grouping * Fixed benchmarks compilation issues * remove explicit evidence when not needed * mostly code formatting * more formatting * refactor all RESP ADTs * widen renamed to coerce where appropriate, more use of context bounds * shuffling things around * more cleanup, wip 2.11 broken * more tests * fixing 2.11 stack overflow in tests * formatting * more cleanup * BaseSpec in core + must => should * no implicit macro * adding transation watch/unwatch * cluster protocol commands * testing more refined types * completed tests on laserdisc-defined refined types * polishing up * removed manual refinements via refined API * removed unuseful RESPBuilder and made Arr a wrapper on List[RESP] instead of Vector[RESP] * no Show syntax, let Bulk handle Show * renamed Extra to Ext(ended) and added a few tests * roundtrip not round-trip * nodes, finally * bye bye 2.11 * minor README fix * more ClusterP testing * add cluster nodes test * cleaning up tests * fixed Host's RFC1123 hostname regex for JS * cluster cleanup * cluster cleanup - removed unnecessary parenthesis * more on geo protocol * completed geo protocol + tests * wip on hash and key protocols * removed last bit of 2.11/2.12 dichotomy * minor cosmetics around geo protocol spec * more hash tests * added glob pattern generator + test and completed hash protocol spec * wip key protocol * still wip key protocol testing * key protocol still missing SORT combinations * added list protocol tests * bump scalacheck to 1.14.0 --- .gitignore | 11 +- .scalafmt.conf | 2 +- .travis.yml | 1 - README.md | 16 +- .../laserdisc/protocol/ProtocolBench.scala | 11 +- .../scala/laserdisc/protocol/RESPBench.scala | 48 +- .../protocol/RESPParamWriteBench.scala | 5 +- .../protocol/UTF8EncodingBench.scala | 3 +- build.sbt | 92 +- .../main/scala/laserdisc/interop/circe.scala | 5 +- .../scala/laserdisc/interop/CirceSpec.scala | 26 +- cli/src/main/scala/laserdisc/cli/CLI.scala | 6 +- .../main/boilerplate/BListExtP.scala.template | 24 + .../boilerplate/BListPExtra.scala.template | 34 - .../main/boilerplate/GeoExtP.scala.template | 21 + ...scala.template => HashExtP.scala.template} | 10 +- ...emplate => HyperLogLogExtP.scala.template} | 2 +- ....scala.template => KeyExtP.scala.template} | 12 +- .../main/boilerplate/ListExtP.scala.template | 14 + .../boilerplate/ListPExtra.scala.template | 16 - ....scala.template => SetExtP.scala.template} | 21 +- ....template => SortedSetExtP.scala.template} | 13 +- ...ala.template => StringExtP.scala.template} | 7 +- .../main/scala-2.11/laserdisc/protocol.scala | 20 - core/src/main/scala/laserdisc/laserdisc.scala | 209 +++++ core/src/main/scala/laserdisc/package.scala | 136 --- .../scala/laserdisc/protocol/BListP.scala | 32 +- .../laserdisc/protocol/BitVectorSyntax.scala | 19 +- .../scala/laserdisc/protocol/ClusterP.scala | 313 +++++++ .../laserdisc/protocol/ConnectionP.scala | 22 +- .../laserdisc/protocol/EitherSyntax.scala | 8 +- .../main/scala/laserdisc/protocol/GeoP.scala | 417 +++++++++ .../main/scala/laserdisc/protocol/HashP.scala | 70 +- .../laserdisc/protocol/HyperLogLogP.scala | 12 +- .../main/scala/laserdisc/protocol/KeyP.scala | 263 +++--- .../protocol/LenientStringCodec.scala | 2 +- .../main/scala/laserdisc/protocol/ListP.scala | 65 +- .../scala/laserdisc/protocol/Protocol.scala | 31 +- .../scala/laserdisc/protocol/PublishP.scala | 2 +- .../main/scala/laserdisc/protocol/RESP.scala | 601 +++++------- .../scala/laserdisc/protocol/RESPFrame.scala | 70 +- .../laserdisc/protocol/RESPParamWrite.scala | 37 +- .../scala/laserdisc/protocol/RESPRead.scala | 46 +- .../main/scala/laserdisc/protocol/Read.scala | 367 ++++---- .../scala/laserdisc/protocol/ServerP.scala | 161 ++-- .../main/scala/laserdisc/protocol/SetP.scala | 83 +- .../main/scala/laserdisc/protocol/Show.scala | 25 +- .../scala/laserdisc/protocol/SortedSetP.scala | 268 +++--- .../scala/laserdisc/protocol/StringP.scala | 122 +-- .../laserdisc/protocol/TransactionP.scala | 10 + core/src/main/scala/laserdisc/syntax.scala | 38 +- core/src/main/scala/laserdisc/types.scala | 19 +- .../boilerplate/BListExtPSpec.scala.template | 103 +++ .../boilerplate/GeoExtPSpec.scala.template | 85 ++ .../boilerplate/HashExtPSpec.scala.template | 51 ++ .../HyperLogLogExtPSpec.scala.template | 47 + .../boilerplate/KeyExtPSpec.scala.template | 105 +++ .../boilerplate/ListExtPSpec.scala.template | 34 + .../test/scala-2.11/laserdisc/HashPSpec.scala | 253 ------ .../test/scala-2.12/laserdisc/HashPSpec.scala | 253 ------ .../src/test/scala/laserdisc/BListPSpec.scala | 145 --- core/src/test/scala/laserdisc/BaseSpec.scala | 148 +++ .../scala/laserdisc/ConnectionPSpec.scala | 126 --- .../scala/laserdisc/HyperLogLogPSpec.scala | 61 -- .../scala/laserdisc/RefinedTypesSpec.scala | 778 ++++++++++++++++ .../scala/laserdisc/protocol/BListPSpec.scala | 96 ++ .../laserdisc/protocol/ClusterPSpec.scala | 421 +++++++++ .../laserdisc/protocol/ConnectionPSpec.scala | 96 ++ .../scala/laserdisc/protocol/GeoPSpec.scala | 857 ++++++++++++++++++ .../scala/laserdisc/protocol/HashPSpec.scala | 277 ++++++ .../laserdisc/protocol/HyperLogLogPSpec.scala | 44 + .../scala/laserdisc/protocol/KeyPSpec.scala | 457 ++++++++++ .../scala/laserdisc/protocol/ListPSpec.scala | 188 ++++ .../laserdisc/protocol/RESPCodecsSpec.scala | 183 ++-- .../laserdisc/protocol/RESPFrameArrSpec.scala | 164 ++++ .../protocol/RESPFrameArraySpec.scala | 154 ---- .../protocol/RESPFrameBulkStringSpec.scala | 192 ++-- .../protocol/RESPFrameMixedSpec.scala | 192 ++-- .../protocol/RESPFunctionsSpec.scala | 53 +- core/src/test/scala/laserdisc/types.scala | 21 + .../main/scala-2.11/laserdisc/fs2/fs2.scala | 21 - .../scala/laserdisc/fs2/ProtocolHandler.scala | 7 +- .../scala/laserdisc/fs2/RedisClient.scala | 3 +- .../main/scala/laserdisc/fs2/Request.scala | 5 +- .../main/scala/laserdisc/fs2/exceptions.scala | 10 +- .../laserdisc/fs2/fs2.scala | 0 .../scala/laserdisc/fs2/RedisClientSpec.scala | 4 +- 87 files changed, 6450 insertions(+), 3052 deletions(-) create mode 100644 core/src/main/boilerplate/BListExtP.scala.template delete mode 100644 core/src/main/boilerplate/BListPExtra.scala.template create mode 100644 core/src/main/boilerplate/GeoExtP.scala.template rename core/src/main/boilerplate/{HashPExtra.scala.template => HashExtP.scala.template} (76%) rename core/src/main/boilerplate/{HyperLogLogPExtra.scala.template => HyperLogLogExtP.scala.template} (91%) rename core/src/main/boilerplate/{KeyPExtra.scala.template => KeyExtP.scala.template} (51%) create mode 100644 core/src/main/boilerplate/ListExtP.scala.template delete mode 100644 core/src/main/boilerplate/ListPExtra.scala.template rename core/src/main/boilerplate/{SetPExtra.scala.template => SetExtP.scala.template} (64%) rename core/src/main/boilerplate/{SortedSetPExtra.scala.template => SortedSetExtP.scala.template} (83%) rename core/src/main/boilerplate/{StringPExtra.scala.template => StringExtP.scala.template} (81%) delete mode 100644 core/src/main/scala-2.11/laserdisc/protocol.scala create mode 100644 core/src/main/scala/laserdisc/laserdisc.scala delete mode 100644 core/src/main/scala/laserdisc/package.scala create mode 100644 core/src/main/scala/laserdisc/protocol/ClusterP.scala create mode 100644 core/src/main/scala/laserdisc/protocol/GeoP.scala create mode 100644 core/src/main/scala/laserdisc/protocol/TransactionP.scala create mode 100644 core/src/test/boilerplate/BListExtPSpec.scala.template create mode 100644 core/src/test/boilerplate/GeoExtPSpec.scala.template create mode 100644 core/src/test/boilerplate/HashExtPSpec.scala.template create mode 100644 core/src/test/boilerplate/HyperLogLogExtPSpec.scala.template create mode 100644 core/src/test/boilerplate/KeyExtPSpec.scala.template create mode 100644 core/src/test/boilerplate/ListExtPSpec.scala.template delete mode 100644 core/src/test/scala-2.11/laserdisc/HashPSpec.scala delete mode 100644 core/src/test/scala-2.12/laserdisc/HashPSpec.scala delete mode 100644 core/src/test/scala/laserdisc/BListPSpec.scala create mode 100644 core/src/test/scala/laserdisc/BaseSpec.scala delete mode 100644 core/src/test/scala/laserdisc/ConnectionPSpec.scala delete mode 100644 core/src/test/scala/laserdisc/HyperLogLogPSpec.scala create mode 100644 core/src/test/scala/laserdisc/RefinedTypesSpec.scala create mode 100644 core/src/test/scala/laserdisc/protocol/BListPSpec.scala create mode 100644 core/src/test/scala/laserdisc/protocol/ClusterPSpec.scala create mode 100644 core/src/test/scala/laserdisc/protocol/ConnectionPSpec.scala create mode 100644 core/src/test/scala/laserdisc/protocol/GeoPSpec.scala create mode 100644 core/src/test/scala/laserdisc/protocol/HashPSpec.scala create mode 100644 core/src/test/scala/laserdisc/protocol/HyperLogLogPSpec.scala create mode 100644 core/src/test/scala/laserdisc/protocol/KeyPSpec.scala create mode 100644 core/src/test/scala/laserdisc/protocol/ListPSpec.scala create mode 100644 core/src/test/scala/laserdisc/protocol/RESPFrameArrSpec.scala delete mode 100644 core/src/test/scala/laserdisc/protocol/RESPFrameArraySpec.scala create mode 100644 core/src/test/scala/laserdisc/types.scala delete mode 100644 fs2/src/main/scala-2.11/laserdisc/fs2/fs2.scala rename fs2/src/main/{scala-2.12 => scala}/laserdisc/fs2/fs2.scala (100%) diff --git a/.gitignore b/.gitignore index f753d6d3..03c4a257 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,13 @@ -**/target/ +# Metals + Bloop +.metals/ +.bloop/ + +# Intellij .idea + +# MacOS *.DS_Store + +# Anything else +**/target/ local.* diff --git a/.scalafmt.conf b/.scalafmt.conf index 9bebc654..fcf28ad5 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,2 +1,2 @@ align = true # For pretty alignment. -maxColumn = 120 # For my wide 30" display. \ No newline at end of file +maxColumn = 140 # For my wide 30" display. diff --git a/.travis.yml b/.travis.yml index b92c15d7..192ad513 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ sudo: false language: scala scala: - 2.12.8 -- 2.11.11-bin-typelevel-4 jdk: - openjdk8 diff --git a/README.md b/README.md index 0ad15c5b..a60fb17e 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,7 @@ LaserDisc is a(nother) Scala driver for [Redis](https://redis.io/), written in S It differentiates itself from the others for having a core layer, which is made up of all the supported Redis commands and the Redis Serialization Protocol ([RESP](https://redis.io/topics/protocol)), that is strongly typed and which makes heavy use of [shapeless](https://github.com/milessabin/shapeless) and [refined](https://github.com/fthomas/refined) to -achieve this. It's also worth noting that the core - in order to be built on scala 2.11.x - makes use of -[Typelevel's Scala 2.11](https://typelevel.org/scala) fork, since it requires the enhancements on implicit heuristics. Finally, it also provides an implementation of RESP built using -[scodec](http://scodec.org/). +achieve this. It also provides an implementation of RESP built using [scodec](http://scodec.org/). On top of this, one or more clients can be implemented. The only one currently available out of the box is built using [fs2](https://functional-streams-for-scala.github.io/fs2/)/[cats effect](https://typelevel.org/cats-effect/) but @@ -48,7 +46,7 @@ Two reasons: ### Getting Started -LaserDisc is currently available for Scala 2.11 and 2.12 on the JVM. +LaserDisc is currently available for Scala 2.12 on the JVM. Its core (protocol commands and RESP wire format) is also available for [Scala.JS](http://www.scala-js.org/). @@ -69,7 +67,7 @@ Support for existing libraries is available via dedicated dependencies. #### [Circe](https://circe.github.io/circe/) When an `io.circe.Decoder[A]` and a `io.circe.Encoder[A]` are implicilty available, -instances of `Show[A]` and `Read[NonNullBulkString, A]` can be derived for free, +instances of `Show[A]` and `Read[Bulk, A]` can be derived for free, just add the following in your `build.sbt`: ``` @@ -136,10 +134,10 @@ This should produce an output similar to the following one: [info] Running Main [info] - [ForkJoinPool-3-worker-2] Starting connection [info] - [ForkJoinPool-3-worker-2] Server available for publishing: localhost:6379 -[debug] - [ForkJoinPool-3-worker-5] sending Array(BulkString(SET),BulkString(a),BulkString(23)) -[debug] - [ForkJoinPool-3-worker-0] receiving SimpleString(OK) -[debug] - [ForkJoinPool-3-worker-1] sending Array(BulkString(GET),BulkString(a)) -[debug] - [ForkJoinPool-3-worker-5] receiving BulkString(23) +[debug] - [ForkJoinPool-3-worker-5] sending Arr(Bulk(SET),Bulk(a),Bulk(23)) +[debug] - [ForkJoinPool-3-worker-0] receiving Str(OK) +[debug] - [ForkJoinPool-3-worker-1] sending Arr(Bulk(GET),Bulk(a)) +[debug] - [ForkJoinPool-3-worker-5] receiving Bulk(23) [info] - [ForkJoinPool-3-worker-2] yay! [info] - [ForkJoinPool-3-worker-2] Shutting down connection [info] - [ForkJoinPool-3-worker-0] Connection terminated: Right(()) diff --git a/benchmarks/core/src/main/scala/laserdisc/protocol/ProtocolBench.scala b/benchmarks/core/src/main/scala/laserdisc/protocol/ProtocolBench.scala index 2857ac70..dbea8f1f 100644 --- a/benchmarks/core/src/main/scala/laserdisc/protocol/ProtocolBench.scala +++ b/benchmarks/core/src/main/scala/laserdisc/protocol/ProtocolBench.scala @@ -1,18 +1,15 @@ -package laserdisc.protocol +package laserdisc +package protocol import org.openjdk.jmh.annotations.{Benchmark, Scope, State} -import eu.timepit.refined.auto._ -import laserdisc.Maybe -import laserdisc.protocol.RESP._ import shapeless._ @State(Scope.Benchmark) class ProtocolBench { - - private final val protocol = Protocol("CUSTOM", _: Int :: String :: Long :: Double :: HNil).as[SimpleString, OK] + private final val protocol = Protocol("CUSTOM", _: Int :: String :: Long :: Double :: HNil).as[Str, OK] private final val request = 0 :: "a" :: 1L :: 2.0d :: HNil - private final val response = str("OK") + private final val response = Str("OK") @Benchmark def encode(): RESP = protocol(request).encode @Benchmark def decode(): Maybe[OK] = protocol(request).decode(response) diff --git a/benchmarks/core/src/main/scala/laserdisc/protocol/RESPBench.scala b/benchmarks/core/src/main/scala/laserdisc/protocol/RESPBench.scala index a4352655..00da2202 100644 --- a/benchmarks/core/src/main/scala/laserdisc/protocol/RESPBench.scala +++ b/benchmarks/core/src/main/scala/laserdisc/protocol/RESPBench.scala @@ -1,8 +1,8 @@ -package laserdisc.protocol +package laserdisc +package protocol import java.nio.charset.StandardCharsets.UTF_8 -import laserdisc.protocol.RESP._ import org.openjdk.jmh.annotations.{Benchmark, Scope, State} import scodec.bits.BitVector import scodec.codecs.utf8 @@ -16,37 +16,37 @@ class RESPBench { private final val chars = 2000 private final val ok = "OK" - private final val okRedis = s"+$ok\r\n" + private final val okRedis = s"+$ok$CRLF" private final val rtProblem = "runtime problem" - private final val rtProblemRedis = s"-$rtProblem\r\n" + private final val rtProblemRedis = s"-$rtProblem$CRLF" private final val fortyTwo = 42L - private final val fortyTwoRedis = s":$fortyTwo\r\n" + private final val fortyTwoRedis = s":$fortyTwo$CRLF" private final val longString = new String(Array.fill(chars)('a')) - private final val longStringRedis = s"$$$chars\r\n$longString\r\n" + private final val longStringRedis = s"$$$chars$CRLF$longString$CRLF" private final val longStringI = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - private final val simpleString = str(ok) - private final val simpleStringBits = BitVector(okRedis.getBytes(UTF_8)) - private final val error = err(rtProblem) - private final val errorBits = BitVector(rtProblemRedis.getBytes(UTF_8)) - private final val integer = int(fortyTwo) - private final val integerBits = BitVector(fortyTwoRedis.getBytes(UTF_8)) - private final val bulkString = bulk(longString) - private final val bulkStringBits = BitVector(longStringRedis.getBytes(UTF_8)) - private final val longStringBits = BitVector(longString.getBytes(UTF_8)) - private final val longStringIBits = BitVector(longStringI.getBytes(UTF_8)) + private final val str = Str(ok) + private final val strBits = BitVector(okRedis.getBytes(UTF_8)) + private final val err = Err(rtProblem) + private final val errBits = BitVector(rtProblemRedis.getBytes(UTF_8)) + private final val num = Num(fortyTwo) + private final val numBits = BitVector(fortyTwoRedis.getBytes(UTF_8)) + private final val bulk = Bulk(longString) + private final val bulkBits = BitVector(longStringRedis.getBytes(UTF_8)) + private final val longStringBits = BitVector(longString.getBytes(UTF_8)) + private final val longStringIBits = BitVector(longStringI.getBytes(UTF_8)) @Benchmark def baseline_utf8_encode: Attempt[BitVector] = utf8.encode(longString) @Benchmark def baseline_utf8_decode: Attempt[String] = utf8.decodeValue(longStringBits) @Benchmark def baseline_utf8_encodeI: Attempt[BitVector] = utf8.encode(longStringI) @Benchmark def baseline_utf8_decodeI: Attempt[String] = utf8.decodeValue(longStringIBits) - @Benchmark def simpleString_encode: Attempt[BitVector] = codec.encode(simpleString) - @Benchmark def simpleString_decode: Attempt[RESP] = codec.decodeValue(simpleStringBits) - @Benchmark def error_encode: Attempt[BitVector] = codec.encode(error) - @Benchmark def error_decode: Attempt[RESP] = codec.decodeValue(errorBits) - @Benchmark def integer_encode: Attempt[BitVector] = codec.encode(integer) - @Benchmark def integer_decode: Attempt[RESP] = codec.decodeValue(integerBits) - @Benchmark def bulkString_encode: Attempt[BitVector] = codec.encode(bulkString) - @Benchmark def bulkString_decode: Attempt[RESP] = codec.decodeValue(bulkStringBits) + @Benchmark def str_encode: Attempt[BitVector] = codec.encode(str) + @Benchmark def str_decode: Attempt[RESP] = codec.decodeValue(strBits) + @Benchmark def err_encode: Attempt[BitVector] = codec.encode(err) + @Benchmark def err_decode: Attempt[RESP] = codec.decodeValue(errBits) + @Benchmark def num_encode: Attempt[BitVector] = codec.encode(num) + @Benchmark def num_decode: Attempt[RESP] = codec.decodeValue(numBits) + @Benchmark def bulk_encode: Attempt[BitVector] = codec.encode(bulk) + @Benchmark def bulk_decode: Attempt[RESP] = codec.decodeValue(bulkBits) } diff --git a/benchmarks/core/src/main/scala/laserdisc/protocol/RESPParamWriteBench.scala b/benchmarks/core/src/main/scala/laserdisc/protocol/RESPParamWriteBench.scala index 9409adf8..bde38a40 100644 --- a/benchmarks/core/src/main/scala/laserdisc/protocol/RESPParamWriteBench.scala +++ b/benchmarks/core/src/main/scala/laserdisc/protocol/RESPParamWriteBench.scala @@ -1,4 +1,5 @@ -package laserdisc.protocol +package laserdisc +package protocol import org.openjdk.jmh.annotations.{Benchmark, Scope, State} import shapeless._ @@ -10,5 +11,5 @@ class RESPParamWriteBench { private final val value = 0 :: "a" :: 1L :: 2.0d :: HNil - @Benchmark def write(): Seq[BulkString] = respParamWrite.write(value) + @Benchmark def write(): Seq[GenBulk] = respParamWrite.write(value) } diff --git a/benchmarks/core/src/main/scala/laserdisc/protocol/UTF8EncodingBench.scala b/benchmarks/core/src/main/scala/laserdisc/protocol/UTF8EncodingBench.scala index d802f280..f3f09c69 100644 --- a/benchmarks/core/src/main/scala/laserdisc/protocol/UTF8EncodingBench.scala +++ b/benchmarks/core/src/main/scala/laserdisc/protocol/UTF8EncodingBench.scala @@ -1,4 +1,5 @@ -package laserdisc.protocol +package laserdisc +package protocol import java.nio.ByteBuffer import java.nio.charset.StandardCharsets.UTF_8 diff --git a/build.sbt b/build.sbt index f2b5c937..619952f0 100644 --- a/build.sbt +++ b/build.sbt @@ -1,44 +1,39 @@ -import sbtcrossproject.CrossPlugin.autoImport.crossProject -import sbtcrossproject.CrossType +// shadow sbt-scalajs' crossProject and CrossType from Scala.js 0.6.x +import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType} -val `scala 211` = "2.11.11-bin-typelevel-4" -val `scala 212` = "2.12.8" +val `scala 2.12` = "2.12.8" val V = new { val circe = "0.11.1" val fs2 = "1.0.5" val `kind-projector` = "0.9.10" val kittens = "1.2.1" + val `log-effect-fs2` = "0.8.0" val refined = "0.9.9" - val refined211 = "0.8.7" - val scalacheck = "1.13.5" + val scalacheck = "1.14.0" val scalatest = "3.0.8" val `scodec-bits` = "1.1.12" val `scodec-core` = "1.11.4" val `scodec-stream` = "1.2.1" val shapeless = "2.3.3" - val `log-effect-fs2` = "0.8.0" } -val `circe-core` = Def.setting("io.circe" %%% "circe-core" % V.circe) -val `circe-parser` = Def.setting("io.circe" %%% "circe-parser" % V.circe) -val `fs2-core` = Def.setting("co.fs2" %%% "fs2-core" % V.fs2) -val `fs2-io` = Def.setting("co.fs2" %% "fs2-io" % V.fs2) -val kittens = Def.setting("org.typelevel" %%% "kittens" % V.kittens) -val `scodec-bits` = Def.setting("org.scodec" %%% "scodec-bits" % V.`scodec-bits`) -val `scodec-core` = Def.setting("org.scodec" %%% "scodec-core" % V.`scodec-core`) -val `scodec-stream` = Def.setting("org.scodec" %%% "scodec-stream" % V.`scodec-stream`) -val shapeless = Def.setting("com.chuusai" %%% "shapeless" % V.shapeless) -val `log-effect-fs2` = Def.setting("io.laserdisc" %%% "log-effect-fs2" % V.`log-effect-fs2`) -val `circe-generic` = Def.setting("io.circe" %%% "circe-generic" % V.circe % Test) -val scalacheck = Def.setting("org.scalacheck" %%% "scalacheck" % V.scalacheck % Test) -val scalatest = Def.setting("org.scalatest" %%% "scalatest" % V.scalatest % Test) -val refined = Def.setting { - is211.value match { - case true => "eu.timepit" %%% "refined" % V.refined211 - case _ => "eu.timepit" %%% "refined" % V.refined - } -} +val `circe-core` = Def.setting("io.circe" %%% "circe-core" % V.circe) +val `circe-parser` = Def.setting("io.circe" %%% "circe-parser" % V.circe) +val `fs2-core` = Def.setting("co.fs2" %%% "fs2-core" % V.fs2) +val `fs2-io` = Def.setting("co.fs2" %% "fs2-io" % V.fs2) +val kittens = Def.setting("org.typelevel" %%% "kittens" % V.kittens) +val `log-effect-fs2` = Def.setting("io.laserdisc" %%% "log-effect-fs2" % V.`log-effect-fs2`) +val refined = Def.setting("eu.timepit" %%% "refined" % V.refined) +val `scodec-bits` = Def.setting("org.scodec" %%% "scodec-bits" % V.`scodec-bits`) +val `scodec-core` = Def.setting("org.scodec" %%% "scodec-core" % V.`scodec-core`) +val `scodec-stream` = Def.setting("org.scodec" %%% "scodec-stream" % V.`scodec-stream`) +val shapeless = Def.setting("com.chuusai" %%% "shapeless" % V.shapeless) + +val `circe-generic` = Def.setting("io.circe" %%% "circe-generic" % V.circe % Test) +val `refined-scalacheck` = Def.setting("eu.timepit" %%% "refined-scalacheck" % V.refined % Test) +val scalacheck = Def.setting("org.scalacheck" %%% "scalacheck" % V.scalacheck % Test) +val scalatest = Def.setting("org.scalatest" %%% "scalatest" % V.scalatest % Test) val `kind-projector-compiler-plugin` = Def.setting { compilerPlugin("org.spire-math" % "kind-projector" % V.`kind-projector` cross CrossVersion.binary) @@ -54,6 +49,7 @@ val coreDeps = Def.Initialize.join { `scodec-core`, shapeless, refined, + `refined-scalacheck`, scalacheck, scalatest ) @@ -65,8 +61,8 @@ val fs2Deps = Def.Initialize.join { `fs2-io`, `kind-projector-compiler-plugin`, kittens, - `scodec-stream`, `log-effect-fs2`, + `scodec-stream`, scalacheck, scalatest ) @@ -113,7 +109,7 @@ val externalApiMappings = Def.task { } val versionDependantScalacOptions = Def.setting { - def versionDependent(scalaVersion: String, flags: Seq[String]) = + def versionDependent(scalaVersion: String, flags: Seq[String]) = CrossVersion.partialVersion(scalaVersion) match { case Some((2, major)) if major >= 12 => flags ++ Seq( @@ -126,11 +122,9 @@ val versionDependantScalacOptions = Def.setting { "-Ywarn-unused:privates", // Warn if a private member is unused. "-Ywarn-value-discard" // Warn when non-Unit expression results are unused. ) - case _ => - (flags ++ Seq("-Yinduction-heuristics", "-Yliteral-types")) - .filterNot(_ == "-Xlint:missing-interpolator") //@implicitNotFound uses ${A} syntax w/o need for s interpolator + case _ => flags } - + val flags = Seq( "-deprecation", // Emit warning and location for usages of deprecated APIs. "-encoding", @@ -168,7 +162,7 @@ val versionDependantScalacOptions = Def.setting { "-Ywarn-infer-any", // Warn when a type argument is inferred to be `Any`. "-Ywarn-nullary-override", // Warn when non-nullary `def f()' overrides nullary `def f'. "-Ywarn-nullary-unit", // Warn when nullary methods return Unit. - "-Ywarn-numeric-widen", // Warn when numerics are widened. + "-Ywarn-numeric-widen" // Warn when numerics are widened. ) versionDependent(scalaVersion.value, flags) @@ -177,17 +171,13 @@ val versionDependantScalacOptions = Def.setting { inThisBuild { Def.settings( organization := "io.laserdisc", - scalaVersion := `scala 212` + scalaVersion := `scala 2.12` ) } lazy val commonSettings = Seq( - scalaOrganization := - (CrossVersion.partialVersion(scalaVersion.value) match { - case Some((2, 11)) => "org.typelevel" - case _ => "org.scala-lang" - }), - crossScalaVersions := Seq(`scala 211`, `scala 212`), + scalaOrganization := "org.scala-lang", + crossScalaVersions := Seq(`scala 2.12`), scalacOptions ++= versionDependantScalacOptions.value, Compile / console / scalacOptions --= Seq("-Ywarn-unused:imports", "-Xfatal-warnings"), Test / console / scalacOptions := (Compile / console / scalacOptions).value @@ -237,18 +227,8 @@ lazy val scoverageSettings = Seq( coverageMinimum := 60, coverageFailOnMinimum := false, coverageHighlighting := true, - coverageEnabled := { - if (is211.value) false else coverageEnabled.value - } ) -lazy val is211 = Def.setting { - CrossVersion.partialVersion(scalaVersion.value) match { - case Some((2, 11)) => true - case _ => false - } -} - lazy val allSettings = commonSettings ++ testSettings ++ scaladocSettings ++ publishSettings ++ scoverageSettings lazy val scalaJsTLSSettings = Seq( @@ -265,7 +245,8 @@ lazy val core = crossProject(JSPlatform, JVMPlatform) .settings( name := "laserdisc-core", libraryDependencies ++= coreDeps.value, - Compile / boilerplateSource := baseDirectory.value.getParentFile / "src" / "main" / "boilerplate" + Compile / boilerplateSource := baseDirectory.value.getParentFile / "src" / "main" / "boilerplate", + Test / boilerplateSource := baseDirectory.value.getParentFile / "src" / "test" / "boilerplate" ) .jvmSettings( javaOptions += "-Djava.net.preferIPv4Stack=true", @@ -311,7 +292,7 @@ lazy val cli = project lazy val circe = crossProject(JSPlatform, JVMPlatform) .withoutSuffixFor(JVMPlatform) - .crossType(CrossType.Pure) + .crossType(CrossType.Pure) .in(file("circe")) .dependsOn(core) .settings(allSettings) @@ -321,13 +302,10 @@ lazy val circe = crossProject(JSPlatform, JVMPlatform) ) .jsSettings(scalaJsTLSSettings: _*) -lazy val circeJVM = circe.jvm -lazy val circeJS = circe.js - lazy val laserdisc = project .in(file(".")) - .aggregate(coreJVM, coreJS, fs2, cli, circeJVM, circeJS) + .aggregate(coreJVM, coreJS, fs2, cli, circe.jvm, circe.js) .settings(publishSettings) .settings( publishArtifact := false - ) \ No newline at end of file + ) diff --git a/circe/src/main/scala/laserdisc/interop/circe.scala b/circe/src/main/scala/laserdisc/interop/circe.scala index 49fe133d..ef47c295 100644 --- a/circe/src/main/scala/laserdisc/interop/circe.scala +++ b/circe/src/main/scala/laserdisc/interop/circe.scala @@ -3,12 +3,11 @@ package interop import io.circe._ import io.circe.syntax._ -import laserdisc.protocol.NonNullBulkString object circe { implicit final def encoderShow[A: Encoder]: Show[A] = Show.instance(_.asJson.noSpaces) - implicit final def decoderRead[A: Decoder]: Read[NonNullBulkString, A] = Read.instance { - case NonNullBulkString(s) => + implicit final def decoderRead[A: Decoder]: Bulk ==> A = Read.instance { + case Bulk(s) => parser.decode(s) match { case Right(a) => Some(a) case _ => None diff --git a/circe/src/test/scala/laserdisc/interop/CirceSpec.scala b/circe/src/test/scala/laserdisc/interop/CirceSpec.scala index 07f2c158..1bc807ff 100644 --- a/circe/src/test/scala/laserdisc/interop/CirceSpec.scala +++ b/circe/src/test/scala/laserdisc/interop/CirceSpec.scala @@ -4,10 +4,8 @@ package interop import io.circe.generic.semiauto._ import io.circe.{Decoder, Encoder} import laserdisc.interop.circe._ -import laserdisc.protocol.NonNullBulkString -import laserdisc.protocol.RESP.bulk import org.scalacheck.{Arbitrary, Gen} -import org.scalatest.{MustMatchers, OptionValues, WordSpec} +import org.scalatest.{Matchers, OptionValues, WordSpec} import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks sealed trait Foo extends Product with Serializable @@ -27,7 +25,7 @@ object Baz { implicit val encoder: Encoder[Baz] = deriveEncoder } -final class CirceSpec extends WordSpec with MustMatchers with ScalaCheckPropertyChecks with OptionValues { +final class CirceSpec extends WordSpec with Matchers with ScalaCheckPropertyChecks with OptionValues { private[this] val barGen: Gen[Bar] = Arbitrary.arbitrary[Int].map(Bar.apply) private[this] val bazGen: Gen[Baz] = for { s <- Arbitrary.arbitrary[String] @@ -40,27 +38,27 @@ final class CirceSpec extends WordSpec with MustMatchers with ScalaCheckProperty "Circe interop" when { - "handling a simple type" must { - "round-trip with no errors" in forAll { bar: Bar => - Read[NonNullBulkString, Bar].read(bulk(Show[Bar].show(bar))).value mustBe bar + "handling a simple type" should { + "roundtrip with no errors" in forAll { bar: Bar => + Read[Bulk, Bar].read(Bulk(bar)).value shouldBe bar } } - "handling a recursive type" must { - "round-trip with no errors" in forAll { baz: Baz => - Read[NonNullBulkString, Baz].read(bulk(Show[Baz].show(baz))).value mustBe baz + "handling a recursive type" should { + "roundtrip with no errors" in forAll { baz: Baz => + Read[Bulk, Baz].read(Bulk(baz)).value shouldBe baz } } - "handling a json that does not respect the contract" must { + "handling a json that does not respect the contract" should { "fail to decode" in { - Read[NonNullBulkString, Bar].read(bulk("""{"i": null}""")) + Read[Bulk, Bar].read(Bulk("""{"i": null}""")) shouldBe empty } } - "handling an invalid json" must { + "handling an invalid json" should { "fail to decode" in { - Read[NonNullBulkString, Bar].read(bulk("{")) + Read[Bulk, Bar].read(Bulk("{")) shouldBe empty } } } diff --git a/cli/src/main/scala/laserdisc/cli/CLI.scala b/cli/src/main/scala/laserdisc/cli/CLI.scala index b95a925f..25cc8c64 100644 --- a/cli/src/main/scala/laserdisc/cli/CLI.scala +++ b/cli/src/main/scala/laserdisc/cli/CLI.scala @@ -122,7 +122,7 @@ object CLI extends IOApp.WithContext { self => }.attempt _.evalMap(mkProtocol).flatMap { - case Left(t) => Stream.eval_(prompt(s"${t.getLocalizedMessage}\n")) + case Left(t) => Stream.eval_(prompt(s"${t.getLocalizedMessage}$LF")) case Right(p) => Stream.emit(p) } } @@ -140,8 +140,8 @@ object CLI extends IOApp.WithContext { self => } yield maybeProtocolResponse.asInstanceOf[Maybe[Any]] -> (endTime - startTime) } .evalMap { - case (Left(t), ToMillis(ms)) => prompt(f"<<< ERROR ${t.getLocalizedMessage} - [$ms%.2fms]\n") - case (Right(response), ToMillis(ms)) => prompt(f"<<< $response - [$ms%.2fms]\n") + case (Left(t), ToMillis(ms)) => prompt(f"<<< ERROR ${t.getLocalizedMessage} - [$ms%.2fms]$LF") + case (Right(response), ToMillis(ms)) => prompt(f"<<< $response - [$ms%.2fms]$LF") } .compile .drain diff --git a/core/src/main/boilerplate/BListExtP.scala.template b/core/src/main/boilerplate/BListExtP.scala.template new file mode 100644 index 00000000..dc3ffa4f --- /dev/null +++ b/core/src/main/boilerplate/BListExtP.scala.template @@ -0,0 +1,24 @@ +package laserdisc +package protocol + +trait BListExtP { this: BListBaseP => + //BLPOP with no timeout specified + [#final def blpop[A: Bulk ==> ?]([#key1: Key#]): Protocol.Aux[Option[KV[A]]] = + blpop[A](OneOrMoreKeys.unsafeFrom([#key1# :: ] :: Nil), NonNegInt(##0))# + ] + + //BLPOP with timeout specified + [..21#final def blpop[A: Bulk ==> ?]([#key1: Key#], seconds: PosInt): Protocol.Aux[Option[KV[A]]] = + blpop[A](OneOrMoreKeys.unsafeFrom([#key1# :: ] :: Nil), NonNegInt.unsafeFrom(seconds.value))# + ] + + //BRPOP with no timeout specified + [#final def brpop[A: Bulk ==> ?]([#key1: Key#]): Protocol.Aux[Option[KV[A]]] = + brpop[A](OneOrMoreKeys.unsafeFrom([#key1# :: ] :: Nil), NonNegInt(##0))# + ] + + //BRPOP with timeout specified + [..21#final def brpop[A: Bulk ==> ?]([#key1: Key#], seconds: PosInt): Protocol.Aux[Option[KV[A]]] = + brpop[A](OneOrMoreKeys.unsafeFrom([#key1# :: ] :: Nil), NonNegInt.unsafeFrom(seconds.value))# + ] +} \ No newline at end of file diff --git a/core/src/main/boilerplate/BListPExtra.scala.template b/core/src/main/boilerplate/BListPExtra.scala.template deleted file mode 100644 index 7da599cf..00000000 --- a/core/src/main/boilerplate/BListPExtra.scala.template +++ /dev/null @@ -1,34 +0,0 @@ -package laserdisc -package protocol - -trait BListPExtra { this: BListP => - import Read.==> - - //BLPOP with no timeout specified - [#final def blpop[A]([#key1: Key#])( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Option[KV[A]]] = - blpop[A](OneOrMoreKeys.unsafeFrom([#key1# :: ] :: Nil), NonNegInt.unsafeFrom(##0))# - ] - - //BLPOP with timeout specified - [..21#final def blpop[A]([#key1: Key#], seconds: PosInt)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Option[KV[A]]] = - blpop[A](OneOrMoreKeys.unsafeFrom([#key1# :: ] :: Nil), NonNegInt.unsafeFrom(seconds.value))# - ] - - //BRPOP with no timeout specified - [#final def brpop[A]([#key1: Key#])( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Option[KV[A]]] = - brpop[A](OneOrMoreKeys.unsafeFrom([#key1# :: ] :: Nil), NonNegInt.unsafeFrom(##0))# - ] - - //BRPOP with timeout specified - [..21#final def brpop[A]([#key1: Key#], seconds: PosInt)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Option[KV[A]]] = - brpop[A](OneOrMoreKeys.unsafeFrom([#key1# :: ] :: Nil), NonNegInt.unsafeFrom(seconds.value))# - ] -} \ No newline at end of file diff --git a/core/src/main/boilerplate/GeoExtP.scala.template b/core/src/main/boilerplate/GeoExtP.scala.template new file mode 100644 index 00000000..01b99512 --- /dev/null +++ b/core/src/main/boilerplate/GeoExtP.scala.template @@ -0,0 +1,21 @@ +package laserdisc +package protocol + +trait GeoExtP { this: GeoBaseP => + import geotypes._ + + //GEOADD + [..21#final def geoadd(key: Key, [#position1: GeoPosition#]): Protocol.Aux[NonNegInt] = + geoadd(key, OneOrMore.unsafeFrom([#position1# :: ] :: Nil))# + ] + + //GEOHASH + [..21#final def geohash(key: Key, [#member1: Key#]): Protocol.Aux[Seq[Option[GeoHash]]] = + geohash(key, OneOrMoreKeys.unsafeFrom([#member1# :: ] :: Nil))# + ] + + //GEOPOS + [..21#final def geopos(key: Key, [#member1: Key#]): Protocol.Aux[Seq[Option[GeoCoordinates]]] = + geopos(key, OneOrMoreKeys.unsafeFrom([#member1# :: ] :: Nil))# + ] +} \ No newline at end of file diff --git a/core/src/main/boilerplate/HashPExtra.scala.template b/core/src/main/boilerplate/HashExtP.scala.template similarity index 76% rename from core/src/main/boilerplate/HashPExtra.scala.template rename to core/src/main/boilerplate/HashExtP.scala.template index 3bb998c2..b9d37477 100644 --- a/core/src/main/boilerplate/HashPExtra.scala.template +++ b/core/src/main/boilerplate/HashExtP.scala.template @@ -1,9 +1,9 @@ package laserdisc package protocol -trait HashPExtra { this: HashP => - import Read.==> +trait HashExtP { this: HashBaseP => import shapeless._ + import shapeless.ops.hlist.Tupler //HDEL [..21#final def hdel(key: Key, [#field1: Key#]): Protocol.Aux[NonNegInt] = @@ -12,12 +12,12 @@ trait HashPExtra { this: HashP => //HMGET final def hmget[A1](key: Key, field1: Key)( - implicit ev: NonNilArray ==> (A1 :: HNil) + implicit ev: Arr ==> (A1 :: HNil) ): Protocol.Aux[A1] = hmget[A1 :: HNil](key, OneOrMoreKeys.unsafeFrom(field1 :: Nil)).map(_.head) [2..21#final def hmget[[#A1#]](key: Key, [#field1: Key#])( - implicit ev0: NonNilArray ==> ([#A1# :: ] :: HNil), - ev1: ops.hlist.Tupler.Aux[[#A1# :: ] :: HNil, ([#A1#])] + implicit ev##0: Arr ==> ([#A1# :: ] :: HNil), + ev##1: Tupler.Aux[[#A1# :: ] :: HNil, ([#A1#])] ): Protocol.Aux[([#A1#])] = hmget[[#A1# :: ] :: HNil](key, OneOrMoreKeys.unsafeFrom([#field1# :: ] :: Nil)).map(_.tupled)# ] diff --git a/core/src/main/boilerplate/HyperLogLogPExtra.scala.template b/core/src/main/boilerplate/HyperLogLogExtP.scala.template similarity index 91% rename from core/src/main/boilerplate/HyperLogLogPExtra.scala.template rename to core/src/main/boilerplate/HyperLogLogExtP.scala.template index 54ce002f..58243e62 100644 --- a/core/src/main/boilerplate/HyperLogLogPExtra.scala.template +++ b/core/src/main/boilerplate/HyperLogLogExtP.scala.template @@ -1,7 +1,7 @@ package laserdisc package protocol -trait HyperLogLogPExtra { this: HyperLogLogP => +trait HyperLogLogExtP { this: HyperLogLogBaseP => //PFADD [..21#final def pfadd(key: Key, [#element1: Key#]): Protocol.Aux[Boolean] = pfadd(key, OneOrMoreKeys.unsafeFrom([#element1# :: ] :: Nil))# diff --git a/core/src/main/boilerplate/KeyPExtra.scala.template b/core/src/main/boilerplate/KeyExtP.scala.template similarity index 51% rename from core/src/main/boilerplate/KeyPExtra.scala.template rename to core/src/main/boilerplate/KeyExtP.scala.template index 12a9e492..31eaca5f 100644 --- a/core/src/main/boilerplate/KeyPExtra.scala.template +++ b/core/src/main/boilerplate/KeyExtP.scala.template @@ -1,7 +1,9 @@ package laserdisc package protocol -trait KeyPExtra { this: KeyP => +trait KeyExtP { this: KeyBaseP => + import keytypes._ + //DEL [#final def del([#key1: Key#]): Protocol.Aux[NonNegInt] = del(OneOrMoreKeys.unsafeFrom([#key1# :: ] :: Nil))# @@ -12,6 +14,14 @@ trait KeyPExtra { this: KeyP => exists(OneOrMoreKeys.unsafeFrom([#key1# :: ] :: Nil))# ] + //MIGRATE + [2..18#final def migrate([#key1: Key#], host: Host, port: Port, dbIndex: DbIndex, timeout: NonNegInt): Protocol.Aux[NOKEY | OK] = + migrate(TwoOrMoreKeys.unsafeFrom([#key1# :: ] :: Nil), host, port, dbIndex, timeout)# + ] + [2..17#final def migrate([#key1: Key#], host: Host, port: Port, dbIndex: DbIndex, timeout: NonNegInt, mode: KeyMigrateMode): Protocol.Aux[NOKEY | OK] = + migrate(TwoOrMoreKeys.unsafeFrom([#key1# :: ] :: Nil), host, port, dbIndex, timeout, mode)# + ] + //TOUCH [#final def touch([#key1: Key#]): Protocol.Aux[NonNegInt] = touch(OneOrMoreKeys.unsafeFrom([#key1# :: ] :: Nil))# diff --git a/core/src/main/boilerplate/ListExtP.scala.template b/core/src/main/boilerplate/ListExtP.scala.template new file mode 100644 index 00000000..0dab94a4 --- /dev/null +++ b/core/src/main/boilerplate/ListExtP.scala.template @@ -0,0 +1,14 @@ +package laserdisc +package protocol + +trait ListExtP { this: ListBaseP => + //LPUSH + [..21#final def lpush[A: Show](key: Key, [#value1: A#]): Protocol.Aux[PosInt] = + lpush(key, OneOrMore.unsafeFrom([#value1# :: ] :: Nil))# + ] + + //RPUSH + [..21#final def rpush[A: Show](key: Key, [#value1: A#]): Protocol.Aux[PosInt] = + rpush(key, OneOrMore.unsafeFrom([#value1# :: ] :: Nil))# + ] +} \ No newline at end of file diff --git a/core/src/main/boilerplate/ListPExtra.scala.template b/core/src/main/boilerplate/ListPExtra.scala.template deleted file mode 100644 index ea557bcf..00000000 --- a/core/src/main/boilerplate/ListPExtra.scala.template +++ /dev/null @@ -1,16 +0,0 @@ -package laserdisc -package protocol - -trait ListPExtra { this: ListP => - import eu.timepit.refined.api.RefinedType - - //LPUSH - [..21#final def lpush[A: Show](key: Key, [#value1: A#]): Protocol.Aux[PosInt] = - lpush(key, RefinedType[OneOrMore[A]].unsafeRefine([#value1# :: ] :: Nil))# - ] - - //RPUSH - [..21#final def rpush[A: Show](key: Key, [#value1: A#]): Protocol.Aux[PosInt] = - rpush(key, RefinedType[OneOrMore[A]].unsafeRefine([#value1# :: ] :: Nil))# - ] -} \ No newline at end of file diff --git a/core/src/main/boilerplate/SetPExtra.scala.template b/core/src/main/boilerplate/SetExtP.scala.template similarity index 64% rename from core/src/main/boilerplate/SetPExtra.scala.template rename to core/src/main/boilerplate/SetExtP.scala.template index d3ddb805..2394994e 100644 --- a/core/src/main/boilerplate/SetPExtra.scala.template +++ b/core/src/main/boilerplate/SetExtP.scala.template @@ -1,19 +1,14 @@ package laserdisc package protocol -trait SetPExtra { this: SetP => - import Read.==> - import eu.timepit.refined.api.RefinedType - +trait SetExtP { this: SetBaseP => //SADD [..21#final def sadd[A: Show](key: Key, [#member1: A#]): Protocol.Aux[NonNegInt] = - sadd(key, RefinedType[OneOrMore[A]].unsafeRefine([#member1# :: ] :: Nil))# + sadd(key, OneOrMore.unsafeFrom([#member1# :: ] :: Nil))# ] //SDIFF - [..21#final def sdiff[A](key##1: Key, [#key2: Key#])( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[A]] = + [..21#final def sdiff[A: Bulk ==> ?](key##1: Key, [#key2: Key#]): Protocol.Aux[Seq[A]] = sdiff[A](TwoOrMoreKeys.unsafeFrom(key##1 :: [#key2# :: ] :: Nil))# ] @@ -23,9 +18,7 @@ trait SetPExtra { this: SetP => ] //SINTER - [..21#final def sinter[A](key##1: Key, [#key2: Key#])( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[A]] = + [..21#final def sinter[A: Bulk ==> ?](key##1: Key, [#key2: Key#]): Protocol.Aux[Seq[A]] = sinter[A](TwoOrMoreKeys.unsafeFrom(key##1 :: [#key2# :: ] :: Nil))# ] @@ -36,13 +29,11 @@ trait SetPExtra { this: SetP => //SREM [..21#final def srem[A: Show](key: Key, [#member1: A#]): Protocol.Aux[NonNegInt] = - srem(key, RefinedType[OneOrMore[A]].unsafeRefine([#member1# :: ] :: Nil))# + srem(key, OneOrMore.unsafeFrom([#member1# :: ] :: Nil))# ] //SUNION - [..21#final def sunion[A](key##1: Key, [#key2: Key#])( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[A]] = + [..21#final def sunion[A: Bulk ==> ?](key##1: Key, [#key2: Key#]): Protocol.Aux[Seq[A]] = sunion[A](TwoOrMoreKeys.unsafeFrom(key##1 :: [#key2# :: ] :: Nil))# ] diff --git a/core/src/main/boilerplate/SortedSetPExtra.scala.template b/core/src/main/boilerplate/SortedSetExtP.scala.template similarity index 83% rename from core/src/main/boilerplate/SortedSetPExtra.scala.template rename to core/src/main/boilerplate/SortedSetExtP.scala.template index 53f18cc2..74203418 100644 --- a/core/src/main/boilerplate/SortedSetPExtra.scala.template +++ b/core/src/main/boilerplate/SortedSetExtP.scala.template @@ -1,28 +1,27 @@ package laserdisc package protocol -trait SortedSetPExtra { this: SortedSetP => +trait SortedSetExtP { this: SortedSetBaseP => import SortedSetP.{Aggregate, Flag} - import eu.timepit.refined.api.RefinedType //ZADD with no flag [..21#final def zadd[A: Show](key: Key, [#member1: A, score1: ValidDouble#]): Protocol.Aux[NonNegInt] = - zadd(key, RefinedType[OneOrMore[(A, ValidDouble)]].unsafeRefine([#(member1 -> score1)# :: ] :: Nil))# + zadd(key, OneOrMore.unsafeFrom([#(member1 -> score1)# :: ] :: Nil))# ] //ZADD with flag [..20#final def zadd[A: Show](key: Key, flag: Flag, [#member1: A, score1: ValidDouble#]): Protocol.Aux[NonNegInt] = - zadd(key, flag, RefinedType[OneOrMore[(A, ValidDouble)]].unsafeRefine([#(member1 -> score1)# :: ] :: Nil))# + zadd(key, flag, OneOrMore.unsafeFrom([#(member1 -> score1)# :: ] :: Nil))# ] //ZADD with no flag changed [..21#final def zaddch[A: Show](key: Key, [#member1: A, score1: ValidDouble#]): Protocol.Aux[NonNegInt] = - zaddch(key, RefinedType[OneOrMore[(A, ValidDouble)]].unsafeRefine([#(member1 -> score1)# :: ] :: Nil))# + zaddch(key, OneOrMore.unsafeFrom([#(member1 -> score1)# :: ] :: Nil))# ] //ZADD with flag changed [..20#final def zaddch[A: Show](key: Key, flag: Flag, [#member1: A, score1: ValidDouble#]): Protocol.Aux[NonNegInt] = - zaddch(key, flag, RefinedType[OneOrMore[(A, ValidDouble)]].unsafeRefine([#(member1 -> score1)# :: ] :: Nil))# + zaddch(key, flag, OneOrMore.unsafeFrom([#(member1 -> score1)# :: ] :: Nil))# ] //ZINTERSTORE with no weights @@ -47,7 +46,7 @@ trait SortedSetPExtra { this: SortedSetP => //ZREM [..21#final def zrem[A: Show](key: Key, [#member1: A#]): Protocol.Aux[NonNegInt] = - zrem(key, RefinedType[OneOrMore[A]].unsafeRefine([#member1# :: ] :: Nil))# + zrem(key, OneOrMore.unsafeFrom([#member1# :: ] :: Nil))# ] //ZUNIONSTORE with no weights diff --git a/core/src/main/boilerplate/StringPExtra.scala.template b/core/src/main/boilerplate/StringExtP.scala.template similarity index 81% rename from core/src/main/boilerplate/StringPExtra.scala.template rename to core/src/main/boilerplate/StringExtP.scala.template index 2217c955..bb64e5cd 100644 --- a/core/src/main/boilerplate/StringPExtra.scala.template +++ b/core/src/main/boilerplate/StringExtP.scala.template @@ -1,9 +1,8 @@ package laserdisc package protocol -trait StringPExtra { this: StringP => +trait StringExtP { this: StringBaseP => import StringP.Bitwise - import Read.==> import shapeless._ //BITOP @@ -12,9 +11,7 @@ trait StringPExtra { this: StringP => ] //MGET - [#final def mget[A]([#key1: Key#])( - implicit ev: NonNilArray ==> A - ): Protocol.Aux[A] = + [#final def mget[A: Arr ==> ?]([#key1: Key#]): Protocol.Aux[A] = mget[A](OneOrMoreKeys.unsafeFrom([#key1# :: ] :: Nil))# ] diff --git a/core/src/main/scala-2.11/laserdisc/protocol.scala b/core/src/main/scala-2.11/laserdisc/protocol.scala deleted file mode 100644 index a54062f5..00000000 --- a/core/src/main/scala-2.11/laserdisc/protocol.scala +++ /dev/null @@ -1,20 +0,0 @@ -package laserdisc - -package object protocol { - - implicit def eitherSyntaxBase[A, B](aOrB: A | B): EitherSyntaxBaseOps[A, B] = - new EitherSyntaxBaseOps(aOrB) -} - -final private[laserdisc] class EitherSyntaxBaseOps[A, B](private val aOrB: A | B) extends AnyVal { - - def flatMap[C](f: B => A | C): A | C = aOrB match { - case left @ Left(_) => left.widenAsRightOf[|, C] - case Right(b) => f(b) - } - - def getOrElse[BB >: B](default: => BB): BB = aOrB match { - case Left(_) => default - case Right(b) => b - } -} diff --git a/core/src/main/scala/laserdisc/laserdisc.scala b/core/src/main/scala/laserdisc/laserdisc.scala new file mode 100644 index 00000000..303f43fd --- /dev/null +++ b/core/src/main/scala/laserdisc/laserdisc.scala @@ -0,0 +1,209 @@ +import java.{lang => j} + +import eu.timepit.refined.W +import eu.timepit.refined.api._ +import eu.timepit.refined.boolean.{And, Not, Or, True} +import eu.timepit.refined.char.Whitespace +import eu.timepit.refined.collection.{Forall, MinSize, MaxSize, NonEmpty} +import eu.timepit.refined.generic.Equal +import eu.timepit.refined.numeric.{Interval, NonNaN, NonNegative, Positive} +import eu.timepit.refined.string.{IPv4, MatchesRegex} +import eu.timepit.refined.types.net.PrivateNetworks._ +import shapeless._ + +package object laserdisc { + // Basic type aliases + final type |[A, B] = Either[A, B] + final type Maybe[A] = Throwable | A + + // Type forwarders + final type Arr = protocol.Arr + final type Bulk = protocol.Bulk + final type Err = protocol.Err + final type NilArr = protocol.NilArr.type + final type NullBulk = protocol.NullBulk.type + final type Num = protocol.Num + final type Str = protocol.Str + final type Protocol = protocol.Protocol + final type ==>[A, B] = protocol.Read[A, B] + final type RESP = protocol.RESP + final type Show[A] = protocol.Show[A] + + // Object forwarders + final val Arr = protocol.Arr + final val Bulk = protocol.Bulk + final val Err = protocol.Err + final val NilArr = protocol.NilArr + final val NullBulk = protocol.NullBulk + final val Num = protocol.Num + final val Protocol = protocol.Protocol + final val Read = protocol.Read + final val Show = protocol.Show + + private[this] final type NoControlChar = Not[ControlChar] + private[this] final type NoWhitespace = Not[Whitespace] + + // Witnesses + final val AllNICsEqWit = W("0.0.0.0") + final val DbIndexMaxValueWit = W(15) + final val GeoHashRegexWit = W("[a-z0-9]{11}") + final val GlobPatternRegexWit = W("(\\[?[\\w\\*\\?]+\\]?)+") //TODO good enough but needs regex' TLC + final val IPv4RegexWit = W("(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)(\\.25[0-5]|2[0-4]\\d|[01]?\\d\\d?){3}") + final val LatitudeMinValueWit = W(-85.05112878D) + final val LatitudeMaxValueWit = W(85.05112878D) + final val LongitudeMinValueWit = W(-180.0D) + final val LongitudeMaxValueWit = W(180.0D) + final val LoopbackEqWit = W("127.0.0.1") + final val NodeIdRegexWit = W("[0-9a-f]{40}") + final val NOKEYEqWit = W("NOKEY") + final val OKEqWit = W("OK") + final val PONGEqWit = W("PONG") + final val PortMinValueWit = W(1024) + final val PortMaxValueWit = W(49151) + final val RangeOffsetMaxValueWit = W(536870911) + final val Rfc1123HostnameMaxLengthWit = W(255) + final val Rfc1123HostnameRegexWit = W( + "(([A-Za-z0-9][\\-A-Za-z0-9]{0,61}[A-Za-z0-9])|[A-Za-z0-9])(\\.(([A-Za-z0-9][\\-A-Za-z0-9]{0,61}[A-Za-z0-9])|[A-Za-z0-9]))*" + ) + final val SlotMaxValueWit = W(16383) + final val StringLengthMaxValueWit = W(4294967295L) + + // Refinements + final type ConnectionNameRef = OneOrMoreRef And Forall[NoWhitespace And NoControlChar] + final type DbIndexRef = Interval.Closed[W.`0`.T, DbIndexMaxValueWit.T] + final type GeoHashRef = MatchesRegex[GeoHashRegexWit.T] + final type GlobPatternRef = MatchesRegex[GlobPatternRegexWit.T] + final type HostRef = Equal[AllNICsEqWit.T] Or + Equal[LoopbackEqWit.T] Or + (Not[IPv4] And MaxSize[Rfc1123HostnameMaxLengthWit.T] And MatchesRegex[Rfc1123HostnameRegexWit.T]) Or + Rfc1918PrivateSpec Or + Rfc5737TestnetSpec Or + Rfc3927LocalLinkSpec Or + Rfc2544BenchmarkSpec + final type IndexRef = True + final type KeyRef = OneOrMoreRef And Forall[NoControlChar] + final type LatitudeRef = Interval.Closed[LatitudeMinValueWit.T, LatitudeMaxValueWit.T] + final type LongitudeRef = Interval.Closed[LongitudeMinValueWit.T, LongitudeMaxValueWit.T] + final type NodeIdRef = MatchesRegex[NodeIdRegexWit.T] + final type NOKEYRef = Equal[NOKEYEqWit.T] + final type NonNegRef = NonNegative + final type NonNegDoubleRef = ValidDoubleRef And NonNegRef + final type NonZeroDoubleRef = ValidDoubleRef And Not[Equal[W.`0.0D`.T]] + final type NonZeroIntRef = Not[Equal[W.`0`.T]] + final type NonZeroLongRef = Not[Equal[W.`0L`.T]] + final type OKRef = Equal[OKEqWit.T] + final type OneOrMoreRef = NonEmpty + final type PONGRef = Equal[PONGEqWit.T] + final type PortRef = Interval.Closed[PortMinValueWit.T, PortMaxValueWit.T] + final type PosRef = Positive + final type RangeOffsetRef = Interval.Closed[W.`0`.T, RangeOffsetMaxValueWit.T] + final type SlotRef = Interval.Closed[W.`0`.T, SlotMaxValueWit.T] + final type StringLengthRef = Interval.Closed[W.`0L`.T, StringLengthMaxValueWit.T] + final type TwoOrMoreRef = MinSize[W.`2`.T] + final type ValidDoubleRef = NonNaN + + // New types + final type ConnectionName = String Refined ConnectionNameRef + final type DbIndex = Int Refined DbIndexRef + final type GeoHash = String Refined GeoHashRef + final type GlobPattern = String Refined GlobPatternRef + final type Host = String Refined HostRef + final type Index = Long Refined IndexRef + final type Key = String Refined KeyRef + final type Latitude = Double Refined LatitudeRef + final type Longitude = Double Refined LongitudeRef + final type NodeId = String Refined NodeIdRef + final type NOKEY = String Refined NOKEYRef + final type NonNegDouble = Double Refined NonNegDoubleRef + final type NonNegInt = Int Refined NonNegRef + final type NonNegLong = Long Refined NonNegRef + final type NonZeroDouble = Double Refined NonZeroDoubleRef + final type NonZeroInt = Int Refined NonZeroIntRef + final type NonZeroLong = Long Refined NonZeroLongRef + final type OK = String Refined OKRef + final type OneOrMore[A] = List[A] Refined OneOrMoreRef + final type OneOrMoreKeys = OneOrMore[Key] + final type PONG = String Refined PONGRef + final type Port = Int Refined PortRef + final type PosInt = Int Refined PosRef + final type PosLong = Long Refined PosRef + final type RangeOffset = Int Refined RangeOffsetRef + final type Slot = Int Refined SlotRef + final type StringLength = Long Refined StringLengthRef + final type TwoOrMoreKeys = List[Key] Refined TwoOrMoreRef + final type TwoOrMoreWeightedKeys = List[(Key, ValidDouble)] Refined TwoOrMoreRef + final type ValidDouble = Double Refined ValidDoubleRef + + // New types' ops + final object OneOrMore { + def from[A](l: List[A])(implicit rt: RefinedType.AuxT[OneOrMore[A], List[A]]): String | OneOrMore[A] = rt.refine(l) + def unapply[A](l: List[A]): Option[OneOrMore[A]] = from(l).right.toOption + def unsafeFrom[A](l: List[A])(implicit rt: RefinedType.AuxT[OneOrMore[A], List[A]]): OneOrMore[A] = rt.unsafeRefine(l) + } + + final object ConnectionName extends RefinedTypeOps[ConnectionName, String] + final object DbIndex extends RefinedTypeOps.Numeric[DbIndex, Int] + final object GeoHash extends RefinedTypeOps[GeoHash, String] + final object GlobPattern extends RefinedTypeOps[GlobPattern, String] + final object Host extends RefinedTypeOps[Host, String] + final object Index extends RefinedTypeOps.Numeric[Index, Long] + final object Key extends RefinedTypeOps[Key, String] + final object Latitude extends RefinedTypeOps.Numeric[Latitude, Double] + final object Longitude extends RefinedTypeOps.Numeric[Longitude, Double] + final object NodeId extends RefinedTypeOps[NodeId, String] + final object NonNegInt extends RefinedTypeOps.Numeric[NonNegInt, Int] + final object NonNegLong extends RefinedTypeOps.Numeric[NonNegLong, Long] + final object NonNegDouble extends RefinedTypeOps.Numeric[NonNegDouble, Double] + final object NonZeroDouble extends RefinedTypeOps.Numeric[NonZeroDouble, Double] + final object NonZeroInt extends RefinedTypeOps.Numeric[NonZeroInt, Int] + final object NonZeroLong extends RefinedTypeOps.Numeric[NonZeroLong, Long] + final object OneOrMoreKeys extends RefinedTypeOps[OneOrMoreKeys, List[Key]] + final object Port extends RefinedTypeOps.Numeric[Port, Int] + final object PosInt extends RefinedTypeOps.Numeric[PosInt, Int] + final object PosLong extends RefinedTypeOps.Numeric[PosLong, Long] + final object RangeOffset extends RefinedTypeOps.Numeric[RangeOffset, Int] + final object Slot extends RefinedTypeOps.Numeric[Slot, Int] + final object StringLength extends RefinedTypeOps.Numeric[StringLength, Long] + final object TwoOrMoreKeys extends RefinedTypeOps[TwoOrMoreKeys, List[Key]] + final object TwoOrMoreWeightedKeys extends RefinedTypeOps[TwoOrMoreWeightedKeys, List[(Key, ValidDouble)]] + final object ValidDouble extends RefinedTypeOps.Numeric[ValidDouble, Double] + + final val LoopbackHost: Host = Host.unsafeFrom(LoopbackEqWit.value) + final val NOKEY: NOKEY = RefType.applyRefM[NOKEY]("NOKEY") + final val OK: OK = RefType.applyRefM[OK]("OK") + final val PONG: PONG = RefType.applyRefM[PONG]("PONG") + + private[laserdisc] final val COMMA_CH = ',' + private[laserdisc] final val LF_CH = '\n' + private[laserdisc] final val SPACE_CH = ' ' + private[laserdisc] final val COMMA = s"$COMMA_CH" + private[laserdisc] final val CRLF = s"\r$LF_CH" + private[laserdisc] final val LF = s"$LF_CH" + private[laserdisc] final val SPACE = s"$SPACE_CH" + + private[laserdisc] final object ToInt { + def unapply(l: Long): Option[Int] = try { Some(j.Math.toIntExact(l)) } catch { case _: ArithmeticException => None } + def unapply(s: String): Option[Int] = try { Some(j.Integer.parseInt(s)) } catch { case _: NumberFormatException => None } + } + private[laserdisc] final object ToLong { + def unapply(s: String): Option[Long] = try { Some(j.Long.parseLong(s)) } catch { case _: NumberFormatException => None } + } + private[laserdisc] final object ToDouble { + def unapply(s: String): Option[Double] = try { Some(j.Double.parseDouble(s)) } catch { case _: NumberFormatException => None } + } + + private[laserdisc] implicit final class WidenOps1[F[_], A](private val fa: F[A]) extends AnyVal { + def widen[AA: <:<[A, ?]: =:!=[A, ?]]: F[AA] = fa.asInstanceOf[F[AA]] + } + + private[laserdisc] implicit final class WidenOps2[F[_, _], A, B](private val fab: F[A, B]) extends AnyVal { + def widenLeft[AA: <:<[A, ?]: =:!=[A, ?]]: F[AA, B] = fab.asInstanceOf[F[AA, B]] + def widenRight[BB: <:<[B, ?]: =:!=[B, ?]]: F[A, BB] = fab.asInstanceOf[F[A, BB]] + def coerceLeft[AA, FF[_, _]](implicit ev: F[AA, B] <:< FF[AA, B]): FF[AA, B] = fab.asInstanceOf[FF[AA, B]] + def coerceRight[FF[_, _], BB](implicit ev: F[A, BB] <:< FF[A, BB]): FF[A, BB] = fab.asInstanceOf[FF[A, BB]] + } + + private[laserdisc] implicit final class WidenOps3[F[_[_], _], G[_], A](private val fga: F[G, A]) extends AnyVal { + def widenRight[AA: <:<[A, ?]: =:!=[A, ?]]: F[G, AA] = fga.asInstanceOf[F[G, AA]] + } +} diff --git a/core/src/main/scala/laserdisc/package.scala b/core/src/main/scala/laserdisc/package.scala deleted file mode 100644 index 9f95e2af..00000000 --- a/core/src/main/scala/laserdisc/package.scala +++ /dev/null @@ -1,136 +0,0 @@ -import java.{lang => j} - -import eu.timepit.refined.W -import eu.timepit.refined.auto._ -import eu.timepit.refined.api._ -import eu.timepit.refined.boolean.{And, Not, Or, True} -import eu.timepit.refined.char.Whitespace -import eu.timepit.refined.collection.{Forall, MinSize, NonEmpty} -import eu.timepit.refined.generic.Equal -import eu.timepit.refined.numeric.Interval.{Closed => ClosedInterval} -import eu.timepit.refined.string.{IPv4, MatchesRegex} -import eu.timepit.refined.types.net.PrivateNetworks._ -import shapeless._ -import shapeless.nat._ - -package object laserdisc { - - type |[A, B] = Either[A, B] - - //type forwarders - final type Protocol = protocol.Protocol - final type Read[A, B] = protocol.Read[A, B] - final type RESP = protocol.RESP - final type Show[A] = protocol.Show[A] - - final type OK = String Refined Equal[W.`"OK"`.T] - final type PONG = String Refined Equal[W.`"PONG"`.T] - - final val OK: OK = "OK" - final val PONG: PONG = "PONG" - - //object forwarders - final val Protocol = protocol.Protocol - final val Read = protocol.Read - final val Show = protocol.Show - - final type Maybe[A] = Throwable | A - - private[this] final type Loopback = IPv4 And Equal[W.`"127.0.0.1"`.T] - private[this] final type RFC1123HostName = MatchesRegex[ - W.`"""^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])(\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9]))*$"""`.T] - - implicit final val nanValidator: Validate.Plain[Double, NaN] = - Validate.fromPredicate(j.Double.isNaN, d => s"($d == NaN)", NaN()) - - private[this] final type NonNaN = Not[NaN] - private[this] final type NonZero = Not[Equal[_0]] - - //shadowed types - final type Key = eu.timepit.refined.types.string.NonEmptyString - final type NonNegInt = eu.timepit.refined.types.numeric.NonNegInt - final type NonNegLong = eu.timepit.refined.types.numeric.NonNegLong - final type Port = eu.timepit.refined.types.net.UserPortNumber - final type PosInt = eu.timepit.refined.types.numeric.PosInt - final type PosLong = eu.timepit.refined.types.numeric.PosLong - - //shadowed types' ops (dispatchers) - final val Key = eu.timepit.refined.types.string.NonEmptyString - final val NonNegInt = eu.timepit.refined.types.numeric.NonNegInt - final val NonNegLong = eu.timepit.refined.types.numeric.NonNegLong - final val Port = eu.timepit.refined.types.net.UserPortNumber - final val PosInt = eu.timepit.refined.types.numeric.PosInt - final val PosLong = eu.timepit.refined.types.numeric.PosLong - - //new types - final type ConnectionName = String Refined (NonEmpty And Forall[Not[Whitespace]]) - final type DbIndex = Int Refined ClosedInterval[_0, _15] - final type GlobPattern = String Refined MatchesRegex[W.`"(\\\\[?[\\\\w\\\\*\\\\?]+\\\\]?)+"`.T] //TODO good enough but needs regex' TLC - final type Host = - String Refined (RFC1123HostName Or Loopback Or Rfc1918PrivateSpec Or Rfc5737TestnetSpec Or Rfc3927LocalLinkSpec Or Rfc2544BenchmarkSpec) - final type Index = Long Refined True - final type NonZeroDouble = Double Refined (NonNaN And NonZero) - final type NonZeroInt = Int Refined NonZero - final type NonZeroLong = Long Refined NonZero - final type OneOrMore[A] = List[A] Refined NonEmpty - final type OneOrMoreKeys = OneOrMore[Key] - final type RangeOffset = Int Refined ClosedInterval[_0, W.`536870911`.T] - final type StringLength = Long Refined ClosedInterval[_0, W.`4294967295L`.T] - final type TwoOrMoreKeys = List[Key] Refined MinSize[_2] - final type TwoOrMoreWeightedKeys = List[(Key, ValidDouble)] Refined MinSize[_2] - final type ValidDouble = Double Refined NonNaN - - //new types' ops - final object OneOrMore { - def from[A](l: List[A])(implicit rt: RefinedType.AuxT[OneOrMore[A], List[A]]): String | OneOrMore[A] = - rt.refine(l) - def unapply[A](l: List[A]): Option[OneOrMore[A]] = from(l).right.toOption - def unsafeFrom[A](l: List[A])(implicit rt: RefinedType.AuxT[OneOrMore[A], List[A]]): OneOrMore[A] = - rt.unsafeRefine(l) - } - - final object ConnectionName extends RefinedTypeOps[ConnectionName, String] - final object DbIndex extends RefinedTypeOps[DbIndex, Int] - final object GlobPattern extends RefinedTypeOps[GlobPattern, String] - final object Host extends RefinedTypeOps[Host, String] - final object Index extends RefinedTypeOps[Index, Long] - final object NonZeroDouble extends RefinedTypeOps[NonZeroDouble, Double] - final object NonZeroInt extends RefinedTypeOps[NonZeroInt, Int] - final object NonZeroLong extends RefinedTypeOps[NonZeroLong, Long] - final object OneOrMoreKeys extends RefinedTypeOps[OneOrMoreKeys, List[Key]] - final object RangeOffset extends RefinedTypeOps[RangeOffset, Int] - final object StringLength extends RefinedTypeOps[StringLength, Long] - final object TwoOrMoreKeys extends RefinedTypeOps[TwoOrMoreKeys, List[Key]] - final object TwoOrMoreWeightedKeys extends RefinedTypeOps[TwoOrMoreWeightedKeys, List[(Key, ValidDouble)]] - final object ValidDouble extends RefinedTypeOps[ValidDouble, Double] - - final object ToInt { - def unapply(l: Long): Option[Int] = - try { Some(j.Math.toIntExact(l)) } catch { case _: ArithmeticException => None } - def unapply(s: String): Option[Int] = - try { Some(j.Integer.parseInt(s)) } catch { case _: NumberFormatException => None } - } - final object ToLong { - def unapply(s: String): Option[Long] = - try { Some(j.Long.parseLong(s)) } catch { case _: NumberFormatException => None } - } - final object ToDouble { - def unapply(s: String): Option[Double] = - try { Some(j.Double.parseDouble(s)) } catch { case _: NumberFormatException => None } - } - - implicit final class WidenOps1[F[_], A](private val fa: F[A]) extends AnyVal { - def widen[AA](implicit ev1: A <:< AA, ev2: A =:!= AA): F[AA] = fa.asInstanceOf[F[AA]] - } - - implicit final class WidenOps2[F[_, _], A, B](private val fab: F[A, B]) extends AnyVal { - def widenLeft[AA](implicit ev1: A <:< AA, ev2: A =:!= AA): F[AA, B] = fab.asInstanceOf[F[AA, B]] - def widenRight[BB](implicit ev1: B <:< BB, ev2: B =:!= BB): F[A, BB] = fab.asInstanceOf[F[A, BB]] - def widenAsLeftOf[AA, FF[_, _]](implicit ev: F[AA, B] <:< FF[AA, B]): FF[AA, B] = fab.asInstanceOf[FF[AA, B]] - def widenAsRightOf[FF[_, _], BB](implicit ev: F[A, BB] <:< FF[A, BB]): FF[A, BB] = fab.asInstanceOf[FF[A, BB]] - } - - implicit final class WidenOps3[F[_[_], _], G[_], A](private val fga: F[G, A]) extends AnyVal { - def widenRight[AA](implicit ev1: A <:< AA, ev2: A =:!= AA): F[G, AA] = fga.asInstanceOf[F[G, AA]] - } -} diff --git a/core/src/main/scala/laserdisc/protocol/BListP.scala b/core/src/main/scala/laserdisc/protocol/BListP.scala index 940aa971..24cd48b0 100644 --- a/core/src/main/scala/laserdisc/protocol/BListP.scala +++ b/core/src/main/scala/laserdisc/protocol/BListP.scala @@ -1,31 +1,19 @@ package laserdisc package protocol -trait BListP { - import Read.==> +trait BListBaseP { import shapeless._ - final def blpop[A](keys: OneOrMoreKeys, seconds: NonNegInt)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Option[KV[A]]] = - Protocol("BLPOP", keys.value :: seconds :: HNil).asC[NilArray :+: NonNilArray :+: CNil, Option[KV[A]]] + final def blpop[A: Bulk ==> ?](keys: OneOrMoreKeys, seconds: NonNegInt): Protocol.Aux[Option[KV[A]]] = + Protocol("BLPOP", keys.value :: seconds :: HNil).opt[GenArr].as[KV[A]] - final def brpop[A](keys: OneOrMoreKeys, seconds: NonNegInt)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Option[KV[A]]] = - Protocol("BRPOP", keys.value :: seconds :: HNil).asC[NilArray :+: NonNilArray :+: CNil, Option[KV[A]]] + final def brpop[A: Bulk ==> ?](keys: OneOrMoreKeys, seconds: NonNegInt): Protocol.Aux[Option[KV[A]]] = + Protocol("BRPOP", keys.value :: seconds :: HNil).opt[GenArr].as[KV[A]] - final def brpoplpush[A](source: Key, destination: Key)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Option[A]] = - Protocol("BRPOPLPUSH", source :: destination :: 0 :: HNil) - .asC[NullBulkString :+: NonNullBulkString :+: CNil, Option[A]] - - final def brpoplpush[A](source: Key, destination: Key, timeout: PosInt)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Option[A]] = - Protocol("BRPOPLPUSH", source :: destination :: timeout :: HNil) - .asC[NullBulkString :+: NonNullBulkString :+: CNil, Option[A]] + final def brpoplpush[A: Bulk ==> ?](source: Key, destination: Key): Protocol.Aux[Option[A]] = + Protocol("BRPOPLPUSH", source :: destination :: 0 :: HNil).opt[GenBulk].as[A] + final def brpoplpush[A: Bulk ==> ?](source: Key, destination: Key, timeout: PosInt): Protocol.Aux[Option[A]] = + Protocol("BRPOPLPUSH", source :: destination :: timeout :: HNil).opt[GenBulk].as[A] } -trait AllBListP extends BListP with BListPExtra +trait BListP extends BListBaseP with BListExtP diff --git a/core/src/main/scala/laserdisc/protocol/BitVectorSyntax.scala b/core/src/main/scala/laserdisc/protocol/BitVectorSyntax.scala index badabe60..e3e85b0e 100644 --- a/core/src/main/scala/laserdisc/protocol/BitVectorSyntax.scala +++ b/core/src/main/scala/laserdisc/protocol/BitVectorSyntax.scala @@ -4,23 +4,16 @@ package protocol import scodec.bits.BitVector private[protocol] trait BitVectorSyntax { - implicit def bitVectorSyntax(bv: BitVector) = new BitVectorSyntaxOps(bv) + implicit def bitVectorSyntax(bv: BitVector): BitVectorSyntaxOps = new BitVectorSyntaxOps(bv) } final private[protocol] class BitVectorSyntaxOps(private val bv: BitVector) extends AnyVal { - /** - * Tries to decode the last `takeRight` bytes of the bit vector as UTF8 text - * If not passed it defaults to 48 bytes + /** Tries to decode the last 48 bytes of the bit vector as UTF-8 text */ - def tailToUtf8(takeRight: Long = 48L): String = { - bv.takeRight(takeRight * 8).decodeUtf8 getOrElse "!! unable to represent the content as UTF8 string !!" - } + def tailToUtf8: String = bv.takeRight(48 * 8).decodeUtf8.getOrElse("content is not UTF-8 encoded") - /** - * Tries to decode the whole bit vector to UTF8 text + /** Tries to decode the whole bit vector to UTF-8 text */ - def toUtf8: String = { - bv.decodeUtf8 getOrElse "!! unable to represent the content as UTF8 string !!" - } -} \ No newline at end of file + def toUtf8: String = bv.decodeUtf8.getOrElse("content is not UTF-8 encoded") +} diff --git a/core/src/main/scala/laserdisc/protocol/ClusterP.scala b/core/src/main/scala/laserdisc/protocol/ClusterP.scala new file mode 100644 index 00000000..98078792 --- /dev/null +++ b/core/src/main/scala/laserdisc/protocol/ClusterP.scala @@ -0,0 +1,313 @@ +package laserdisc +package protocol + +object ClusterP { + import scala.language.dynamics + + private[protocol] final val KVPairRegex = "(.*):(.*)".r + + final class Info(private val properties: Map[String, String]) extends AnyVal with Dynamic { + def selectDynamic[A](field: String)(implicit R: String ==> A): Maybe[A] = + properties.get(field).flatMap(R.read).toRight(Err(s"no key $field of the provided type found")) + } + final object Info { + implicit final val infoRead: Bulk ==> Info = Read.instancePF { + case Bulk(s) => new Info(s.split(CRLF).collect { case KVPairRegex(k, v) => k -> v }.toMap) + } + } + + final case class Node( + id: NodeId, + address: Address, + flags: Seq[Flag], + maybeMaster: Option[NodeId], + ping: NonNegInt, + pong: NonNegInt, + configEpoch: NonNegInt, + link: LinkState, + slots: Seq[SlotType] + ) + final case class Nodes(nodes: Seq[Node]) extends AnyVal + final object Nodes { + private final val A: String ==> Address = { + val HPCP = raw"([A-Za-z0-9\-\.]*):(\d+)@(\d+)".r + val HP = raw"([A-Za-z0-9\-\.]*):(\d+)".r + Read.instancePF { + case HPCP("", ToInt(Port(p)), ToInt(Port(cp))) => Address(LoopbackHost, p, cp) + case HPCP(Host(h), ToInt(Port(p)), ToInt(Port(cp))) => Address(h, p, cp) + case HP("", ToInt(Port(p))) => Address(LoopbackHost, p, p) + case HP(Host(h), ToInt(Port(p))) => Address(h, p, p) + } + } + private final val Fs: String ==> Seq[Flag] = Read.instancePF { + case "noflags" => Seq.empty + case s => + s.split(COMMA_CH).toList.collect { + case "myself" => Flag.myself + case "master" => Flag.master + case "slave" => Flag.replica + case "fail?" => Flag.possiblefail + case "fail" => Flag.fail + case "handshake" => Flag.handshake + case "noaddr" => Flag.noaddress + } + } + private final val MM: String ==> Option[NodeId] = Read.instancePF { + case "-" => None + case NodeId(id) => Some(id) + } + private final val L: String ==> LinkState = Read.instancePF { + case "connected" => LinkState.connected + case "disconnected" => LinkState.disconnected + } + private final val Ss: Seq[String] ==> Seq[SlotType] = { + val R = raw"(\d+)-(\d+)".r + val IS = raw"\[(\d+)-<-(${NodeIdRegexWit.value})\]".r + val MS = raw"\[(\d+)->-(${NodeIdRegexWit.value})\]".r + Read.instancePF { + case ss => + ss.collect { + case ToInt(Slot(s)) => SlotType.Single(s) + case R(ToInt(Slot(f)), ToInt(Slot(t))) => SlotType.Range(f, t) + case IS(ToInt(Slot(s)), NodeId(id)) => SlotType.ImportingSlot(s, id) + case MS(ToInt(Slot(s)), NodeId(id)) => SlotType.MigratingSlot(s, id) + }.toList + } + } + + implicit final val nodesRead: Bulk ==> Nodes = Read.instancePF { + case Bulk(s) => + Nodes( + s.split(LF_CH) + .flatMap { + _.split(SPACE_CH).toSeq match { + case NodeId(id) +: A(a) +: Fs(fs) +: MM(mm) +: + ToInt(NonNegInt(ps)) +: ToInt(NonNegInt(pr)) +: ToInt(NonNegInt(ce)) +: L(l) +: Ss(ss) => + Some(Node(id, a, fs, mm, ps, pr, ce, l, ss)) + case _ => None + } + } + .toList + ) + } + } + + sealed trait FailoverMode + final object FailoverMode { + final case object force extends FailoverMode + final case object takeover extends FailoverMode + + implicit final val failoverModeShow: Show[FailoverMode] = Show.instance { + case `force` => "FORCE" + case `takeover` => "TAKEOVER" + } + } + + sealed trait Flag + final object Flag { + final case object myself extends Flag + final case object master extends Flag + final case object replica extends Flag + final case object possiblefail extends Flag + final case object fail extends Flag + final case object handshake extends Flag + final case object noaddress extends Flag + + implicit final val flagShow: Show[Flag] = Show.instance { + case `myself` => "myself" + case `master` => "master" + case `replica` => "slave" + case `possiblefail` => "fail?" + case `fail` => "fail" + case `handshake` => "handshake" + case `noaddress` => "noaddr" + } + } + + final case class HostPort(host: Host, port: Port) + final case class HostPortNodeId(host: Host, port: Port, nodeId: NodeId) + + sealed trait LinkState + final object LinkState { + final case object connected extends LinkState + final case object disconnected extends LinkState + + implicit final val linkStateShow: Show[LinkState] = Show.unsafeFromToString + } + + final case class Address(host: Host, port: Port, clusterPort: Port) + + sealed trait ResetMode + final object ResetMode { + final case object hard extends ResetMode + final case object soft extends ResetMode + + implicit final val resetModeShow: Show[ResetMode] = Show.instance { + case `hard` => "HARD" + case `soft` => "SOFT" + } + } + + sealed trait SetSlotMode + final object SetSlotMode { + final case object importing extends SetSlotMode + final case object migrating extends SetSlotMode + final case object node extends SetSlotMode + + implicit final val setSlotModeShow: Show[SetSlotMode] = Show.instance { + case `importing` => "IMPORTING" + case `migrating` => "MIGRATING" + case `node` => "NODE" + } + } + + sealed trait SlotInfo + final object SlotInfo { + final case class NewSlotInfo(master: HostPortNodeId, replicas: Seq[HostPortNodeId]) extends SlotInfo + final case class OldSlotInfo(master: HostPort, replicas: Seq[HostPort]) extends SlotInfo + } + + sealed trait SlotType + final object SlotType { + final case class Single(slot: Slot) extends SlotType + final case class Range(from: Slot, to: Slot) extends SlotType + final case class ImportingSlot(slot: Slot, fromNodeId: NodeId) extends SlotType + final case class MigratingSlot(slot: Slot, toNodeId: NodeId) extends SlotType + } + + final case class Slots(slots: Map[SlotType.Range, SlotInfo]) extends AnyVal { + final def infoFor(slot: Slot): Option[SlotInfo] = + slots + .find { + case (SlotType.Range(from, to), _) => slot.value >= from.value && slot.value <= to.value + } + .map { case (_, value) => value } + } + final object Slots { + import SlotInfo._ + import SlotType.Range + private val H: Bulk ==> Host = Read.instancePF { + case Bulk("") => LoopbackHost + case Bulk(Host(h)) => h + } + private val NSI: Seq[RESP] ==> NewSlotInfo = { + val HPNIs: Seq[RESP] ==> Seq[HostPortNodeId] = Read.instancePF { + case arr => arr.collect { case Arr(H(h) +: Num(ToInt(Port(p))) +: Bulk(NodeId(id)) +: Seq()) => HostPortNodeId(h, p, id) }.toList + } + Read.instancePF { + case Arr(H(h) +: Num(ToInt(Port(p))) +: Bulk(NodeId(id)) +: Seq()) +: HPNIs(rs) => NewSlotInfo(HostPortNodeId(h, p, id), rs) + } + } + private val OSI: Seq[RESP] ==> OldSlotInfo = { + val HPs: Seq[RESP] ==> Seq[HostPort] = Read.instancePF { + case arr => arr.collect { case Arr(H(h) +: Num(ToInt(Port(p))) +: Seq()) => HostPort(h, p) }.toList + } + Read.instancePF { + case Arr(H(h) +: Num(ToInt(Port(p))) +: Seq()) +: HPs(rs) => OldSlotInfo(HostPort(h, p), rs) + } + } + + implicit final val slotsRead: Arr ==> Slots = Read.instancePF { + case arr => + Slots(arr.elements.collect { + case Arr(Num(ToInt(Slot(from))) +: Num(ToInt(Slot(to))) +: OSI(osi)) => Range(from, to) -> osi + case Arr(Num(ToInt(Slot(from))) +: Num(ToInt(Slot(to))) +: NSI(nsi)) => Range(from, to) -> nsi + }.toMap) + } + } +} + +trait ClusterP { + import shapeless._ + + final object clustertypes { + final type ClusterAddress = ClusterP.Address + final type ClusterFailoverMode = ClusterP.FailoverMode + final type ClusterFlag = ClusterP.Flag + final type ClusterHostPort = ClusterP.HostPort + final type ClusterHostPortNodeId = ClusterP.HostPortNodeId + final type ClusterImportingSlotType = ClusterP.SlotType.ImportingSlot + final type ClusterInfo = ClusterP.Info + final type ClusterLinkState = ClusterP.LinkState + final type ClusterMigratingSlotType = ClusterP.SlotType.MigratingSlot + final type ClusterNewSlotInfo = ClusterP.SlotInfo.NewSlotInfo + final type ClusterNode = ClusterP.Node + final type ClusterNodes = ClusterP.Nodes + final type ClusterOldSlotInfo = ClusterP.SlotInfo.OldSlotInfo + final type ClusterRangeSlotType = ClusterP.SlotType.Range + final type ClusterResetMode = ClusterP.ResetMode + final type ClusterSetSlotMode = ClusterP.SetSlotMode + final type ClusterSingleSlotType = ClusterP.SlotType.Single + final type ClusterSlots = ClusterP.Slots + + final val ClusterAddress = ClusterP.Address + final val ClusterFailoverMode = ClusterP.FailoverMode + final val ClusterFlag = ClusterP.Flag + final val ClusterHostPort = ClusterP.HostPort + final val ClusterHostPortNodeId = ClusterP.HostPortNodeId + final val ClusterImportingSlotType = ClusterP.SlotType.ImportingSlot + final val ClusterInfo = ClusterP.Info + final val ClusterLinkState = ClusterP.LinkState + final val ClusterMigratingSlotType = ClusterP.SlotType.MigratingSlot + final val ClusterNewSlotInfo = ClusterP.SlotInfo.NewSlotInfo + final val ClusterNode = ClusterP.Node + final val ClusterNodes = ClusterP.Nodes + final val ClusterOldSlotInfo = ClusterP.SlotInfo.OldSlotInfo + final val ClusterRangeSlotType = ClusterP.SlotType.Range + final val ClusterResetMode = ClusterP.ResetMode + final val ClusterSetSlotMode = ClusterP.SetSlotMode + final val ClusterSingleSlotType = ClusterP.SlotType.Single + final val ClusterSlots = ClusterP.Slots + } + + import clustertypes._ + + final def addslots(slots: OneOrMore[Slot]): Protocol.Aux[OK] = Protocol("CLUSTER", "ADDSLOTS" :: slots.value :: HNil).as[Str, OK] + + final val clusterinfo: Protocol.Aux[ClusterInfo] = Protocol("CLUSTER", "INFO").as[Bulk, ClusterInfo] + + final def countfailurereports(nodeId: NodeId): Protocol.Aux[NonNegInt] = + Protocol("CLUSTER", "COUNT-FAILURE-REPORTS" :: nodeId :: HNil).as[Num, NonNegInt] + + final def countkeysinslot(slot: Slot): Protocol.Aux[NonNegInt] = Protocol("CLUSTER", "COUNTKEYSINSLOT" :: slot :: HNil).as[Num, NonNegInt] + + final def delslots(slots: OneOrMore[Slot]): Protocol.Aux[OK] = Protocol("CLUSTER", "DELSLOTS" :: slots.value :: HNil).as[Str, OK] + + final val failover: Protocol.Aux[OK] = Protocol("CLUSTER", "FAILOVER").as[Str, OK] + final def failover(mode: ClusterFailoverMode): Protocol.Aux[OK] = Protocol("CLUSTER", "FAILOVER" :: mode :: HNil).as[Str, OK] + + final def forget(nodeId: NodeId): Protocol.Aux[OK] = Protocol("CLUSTER", "FORGET" :: nodeId :: HNil).as[Str, OK] + + final def getkeysinslot(slot: Slot, count: PosInt): Protocol.Aux[Seq[Key]] = + Protocol("CLUSTER", "GETKEYSINSLOT" :: slot :: count :: HNil).as[Arr, Seq[Key]] + + final def keyslot(key: Key): Protocol.Aux[Slot] = Protocol("CLUSTER", "KEYSLOT" :: key :: HNil).as[Num, Slot] + + final def meet(host: Host, port: Port): Protocol.Aux[OK] = Protocol("CLUSTER", "MEET" :: host :: port :: HNil).as[Str, OK] + + final val nodes: Protocol.Aux[ClusterNodes] = Protocol("CLUSTER", "NODES").as[Bulk, ClusterNodes] + + final val readonly: Protocol.Aux[OK] = Protocol("CLUSTER", "READONLY").as[Str, OK] + + final val readwrite: Protocol.Aux[OK] = Protocol("CLUSTER", "READWRITE").as[Str, OK] + + final def replicas(nodeId: NodeId): Protocol.Aux[ClusterNodes] = Protocol("CLUSTER", "REPLICAS" :: nodeId :: HNil).as[Bulk, ClusterNodes] + + final def replicate(nodeId: NodeId): Protocol.Aux[OK] = Protocol("CLUSTER", "REPLICATE" :: nodeId :: HNil).as[Str, OK] + + final def reset(mode: ClusterResetMode): Protocol.Aux[OK] = Protocol("CLUSTER", "RESET" :: mode :: HNil).as[Str, OK] + final val reset: Protocol.Aux[OK] = reset(ClusterResetMode.soft) + + final val saveconfig: Protocol.Aux[OK] = Protocol("CLUSTER", "SAVECONFIG").as[Str, OK] + + final def setconfigepoch(configEpoch: NonNegInt): Protocol.Aux[OK] = + Protocol("CLUSTER", "SET-CONFIG-EPOCH" :: configEpoch :: HNil).as[Str, OK] + + final def setslot(slot: Slot): Protocol.Aux[OK] = Protocol("CLUSTER", "SETSLOT" :: slot :: "STABLE" :: HNil).as[Str, OK] + final def setslot(slot: Slot, mode: ClusterSetSlotMode, node: NodeId): Protocol.Aux[OK] = + Protocol("CLUSTER", "SETSLOT" :: slot :: mode :: node :: HNil).as[Str, OK] + + final def slaves(nodeId: NodeId): Protocol.Aux[ClusterNodes] = Protocol("CLUSTER", "SLAVES" :: nodeId :: HNil).as[Bulk, ClusterNodes] + + final val slots: Protocol.Aux[ClusterSlots] = Protocol("CLUSTER", "SLOTS").as[Arr, ClusterSlots] +} diff --git a/core/src/main/scala/laserdisc/protocol/ConnectionP.scala b/core/src/main/scala/laserdisc/protocol/ConnectionP.scala index 00810e86..ce1bb025 100644 --- a/core/src/main/scala/laserdisc/protocol/ConnectionP.scala +++ b/core/src/main/scala/laserdisc/protocol/ConnectionP.scala @@ -2,24 +2,18 @@ package laserdisc package protocol trait ConnectionP { - import Read.==> + private[this] implicit final val pongRead: Str ==> PONG = Read.instancePF { case Str(PONG.`value`) => PONG } - private[this] implicit final val pongRead: SimpleString ==> PONG = Read.instancePF { - case SimpleString("PONG") => PONG - } + final def auth(password: Key): Protocol.Aux[OK] = Protocol("AUTH", password).as[Str, OK] - final def auth(password: Key): Protocol.Aux[OK] = Protocol("AUTH", password).as[SimpleString, OK] + final def echo[A: Show: Bulk ==> ?](message: A): Protocol.Aux[A] = Protocol("ECHO", message).as[Bulk, A] - final def echo(message: Key): Protocol.Aux[Key] = Protocol("ECHO", message).as[NonNullBulkString, Key] + final val ping: Protocol.Aux[PONG] = Protocol("PING", Nil).as[Str, PONG] + final def ping[A: Show: Bulk ==> ?](message: A): Protocol.Aux[A] = Protocol("PING", message).as[Bulk, A] - final val ping: Protocol.Aux[PONG] = Protocol("PING", Nil).as[SimpleString, PONG] + final val quit: Protocol.Aux[OK] = Protocol("QUIT", Nil).as[Str, OK] - final def ping(message: Key): Protocol.Aux[Key] = Protocol("PING", message).as[NonNullBulkString, Key] + final def select(index: DbIndex): Protocol.Aux[OK] = Protocol("SELECT", index).as[Str, OK] - final val quit: Protocol.Aux[OK] = Protocol("QUIT", Nil).as[SimpleString, OK] - - final def select(index: DbIndex): Protocol.Aux[OK] = Protocol("SELECT", index).as[SimpleString, OK] - - final def swapdb(index1: DbIndex, index2: DbIndex): Protocol.Aux[OK] = - Protocol("SWAPDB", index1 :: index2 :: Nil).as[SimpleString, OK] + final def swapdb(index1: DbIndex, index2: DbIndex): Protocol.Aux[OK] = Protocol("SWAPDB", index1 :: index2 :: Nil).as[Str, OK] } diff --git a/core/src/main/scala/laserdisc/protocol/EitherSyntax.scala b/core/src/main/scala/laserdisc/protocol/EitherSyntax.scala index 563a0559..d7b1f3ac 100644 --- a/core/src/main/scala/laserdisc/protocol/EitherSyntax.scala +++ b/core/src/main/scala/laserdisc/protocol/EitherSyntax.scala @@ -7,13 +7,13 @@ private[protocol] trait EitherSyntax { } final private[protocol] class EitherValuesSyntaxOps[A](private val a: A) extends AnyVal { - def asLeft[B]: A | B = Left(a).widenAsRightOf[|, B] - def asRight[B]: B | A = Right(a).widenAsLeftOf[B, |] + def asLeft[B]: A | B = Left(a) + def asRight[B]: B | A = Right(a) } final private[protocol] class EitherSyntaxOps[A, B](private val aOrB: A | B) extends AnyVal { def leftMap[C](f: A => C): C | B = aOrB match { - case Left(a) => Left(f(a)) - case r @ Right(_) => r.widenAsLeftOf[C, |] + case Left(a) => Left(f(a)) + case right => right.coerceLeft[C, |] } } diff --git a/core/src/main/scala/laserdisc/protocol/GeoP.scala b/core/src/main/scala/laserdisc/protocol/GeoP.scala new file mode 100644 index 00000000..e8a23b74 --- /dev/null +++ b/core/src/main/scala/laserdisc/protocol/GeoP.scala @@ -0,0 +1,417 @@ +package laserdisc +package protocol + +object GeoP { + final case class Coordinates(latitude: Latitude, longitude: Longitude) + final object Coordinates { + implicit final val coordinatesRead: Arr ==> Coordinates = Read.instancePF { + case Arr(Bulk(ToDouble(Longitude(long))) +: Bulk(ToDouble(Latitude(lat))) +: Seq()) => Coordinates(lat, long) + } + } + + final case class Position(member: Key, latitude: Latitude, longitude: Longitude) + + final case class KeyAndCoordinates(key: Key, coordinates: Coordinates) + final case class KeyAndDistance(key: Key, distance: NonNegDouble) + final case class KeyAndHash(key: Key, hash: NonNegLong) + final case class KeyCoordinatesAndDistance(key: Key, coordinates: Coordinates, distance: NonNegDouble) + final case class KeyCoordinatesAndHash(key: Key, coordinates: Coordinates, hash: NonNegLong) + final case class KeyDistanceAndHash(key: Key, distance: NonNegDouble, hash: NonNegLong) + final case class KeyCoordinatesDistanceAndHash(key: Key, coordinates: Coordinates, distance: NonNegDouble, hash: NonNegLong) + + sealed trait RadiusMode { type Res; def params: List[String]; def r: Arr ==> Res } + object RadiusMode { + final object coordinates extends RadiusMode { + override final type Res = KeyAndCoordinates + override final val params: List[String] = List("WITHCOORD") + override final val r: Arr ==> Res = radiusModeCoordinatesRead + + def &(d: distance.type): coordinatesAndDistance.type = { val _ = d; coordinatesAndDistance } + def &(h: hash.type): coordinatesAndHash.type = { val _ = h; coordinatesAndHash } + } + final object distance extends RadiusMode { + override final type Res = KeyAndDistance + override final val params: List[String] = List("WITHDIST") + override final val r: Arr ==> Res = radiusModeDistanceRead + + def &(c: coordinates.type): coordinatesAndDistance.type = { val _ = c; coordinatesAndDistance } + def &(h: hash.type): distanceAndHash.type = { val _ = h; distanceAndHash } + } + final object hash extends RadiusMode { + override final type Res = KeyAndHash + override final val params: List[String] = List("WITHHASH") + override final val r: Arr ==> Res = radiusModeHashRead + + def &(c: coordinates.type): coordinatesAndHash.type = { val _ = c; coordinatesAndHash } + def &(c: distance.type): distanceAndHash.type = { val _ = c; distanceAndHash } + } + final object coordinatesAndDistance extends RadiusMode { + override final type Res = KeyCoordinatesAndDistance + override final val params: List[String] = List("WITHCOORD", "WITHDIST") + override final val r: Arr ==> Res = radiusModeCoordinatesAndDistanceRead + + def &(h: hash.type): all.type = { val _ = h; all } + } + final object coordinatesAndHash extends RadiusMode { + override final type Res = KeyCoordinatesAndHash + override final val params: List[String] = List("WITHCOORD", "WITHHASH") + override final val r: Arr ==> Res = radiusModeCoordinatesAndHashRead + + def &(d: distance.type): all.type = { val _ = d; all } + } + final object distanceAndHash extends RadiusMode { + override final type Res = KeyDistanceAndHash + override final val params: List[String] = List("WITHDIST", "WITHHASH") + override final val r: Arr ==> Res = radiusModeDistanceAndHashRead + + def &(c: coordinates.type): all.type = { val _ = c; all } + } + final object all extends RadiusMode { + override final type Res = KeyCoordinatesDistanceAndHash + override final val params: List[String] = List("WITHCOORD", "WITHDIST", "WITHHASH") + override final val r: Arr ==> Res = radiusModeAllRead + } + } + + sealed trait StoreMode { def params: List[String] } + object StoreMode { + final case class Hash(key: Key) extends StoreMode { override final val params: List[String] = List("STORE", key.value) } + final case class Distance(key: Key) extends StoreMode { override final val params: List[String] = List("STOREDIST", key.value) } + final case class Both(hashKey: Key, distanceKey: Key) extends StoreMode { + override final val params: List[String] = List("STORE", hashKey.value, "STOREDIST", distanceKey.value) + } + } + + sealed trait Unit + final object Unit { + final case object meters extends Unit + final case object kilometers extends Unit + final case object miles extends Unit + final case object feet extends Unit + + implicit val unitShow: Show[Unit] = Show.instance { + case `meters` => "m" + case `kilometers` => "km" + case `miles` => "mi" + case `feet` => "ft" + } + } + + implicit final val radiusModeCoordinatesRead: Arr ==> RadiusMode.coordinates.Res = Read.instancePF { + case Arr(Bulk(Key(k)) +: Arr(Bulk(ToDouble(Longitude(long))) +: Bulk(ToDouble(Latitude(lat))) +: Seq()) +: Seq()) => + KeyAndCoordinates(k, Coordinates(lat, long)) + } + implicit final val radiusModeDistanceRead: Arr ==> RadiusMode.distance.Res = Read.instancePF { + case Arr(Bulk(Key(k)) +: Bulk(ToDouble(NonNegDouble(d))) +: Seq()) => KeyAndDistance(k, d) + } + implicit final val radiusModeHashRead: Arr ==> RadiusMode.hash.Res = Read.instancePF { + case Arr(Bulk(Key(k)) +: Num(NonNegLong(l)) +: Seq()) => KeyAndHash(k, l) + } + implicit final val radiusModeCoordinatesAndDistanceRead: Arr ==> RadiusMode.coordinatesAndDistance.Res = Read.instancePF { + case Arr( + Bulk(Key(k)) +: Bulk(ToDouble(NonNegDouble(d))) +: + Arr(Bulk(ToDouble(Longitude(long))) +: Bulk(ToDouble(Latitude(lat))) +: Seq()) +: Seq() + ) => + KeyCoordinatesAndDistance(k, Coordinates(lat, long), d) + } + implicit final val radiusModeCoordinatesAndHashRead: Arr ==> RadiusMode.coordinatesAndHash.Res = Read.instancePF { + case Arr( + Bulk(Key(k)) +: Num(NonNegLong(l)) +: + Arr(Bulk(ToDouble(Longitude(long))) +: Bulk(ToDouble(Latitude(lat))) +: Seq()) +: Seq() + ) => + KeyCoordinatesAndHash(k, Coordinates(lat, long), l) + } + implicit final val radiusModeDistanceAndHashRead: Arr ==> RadiusMode.distanceAndHash.Res = Read.instancePF { + case Arr(Bulk(Key(k)) +: Bulk(ToDouble(NonNegDouble(d))) +: Num(NonNegLong(l)) +: Seq()) => KeyDistanceAndHash(k, d, l) + } + implicit final val radiusModeAllRead: Arr ==> RadiusMode.all.Res = Read.instancePF { + case Arr( + Bulk(Key(k)) +: Bulk(ToDouble(NonNegDouble(d))) +: Num(NonNegLong(l)) +: + Arr(Bulk(ToDouble(Longitude(long))) +: Bulk(ToDouble(Latitude(lat))) +: Seq()) +: Seq() + ) => + KeyCoordinatesDistanceAndHash(k, Coordinates(lat, long), d, l) + } +} + +trait GeoBaseP { + import shapeless._ + + final object geotypes { + final type GeoCoordinates = GeoP.Coordinates + final type GeoKeyAndCoord = GeoP.KeyAndCoordinates + final type GeoKeyAndDist = GeoP.KeyAndDistance + final type GeoKeyAndHash = GeoP.KeyAndHash + final type GeoKeyCoordAndDist = GeoP.KeyCoordinatesAndDistance + final type GeoKeyCoordAndHash = GeoP.KeyCoordinatesAndHash + final type GeoKeyDistAndHash = GeoP.KeyDistanceAndHash + final type GeoKeyCoordDistAndHash = GeoP.KeyCoordinatesDistanceAndHash + final type GeoPosition = GeoP.Position + final type GeoRadiusMode = GeoP.RadiusMode + final type GeoStoreMode = GeoP.StoreMode + final type GeoUnit = GeoP.Unit + + final val GeoCoordinates = GeoP.Coordinates + final val GeoKeyAndCoord = GeoP.KeyAndCoordinates + final val GeoKeyAndDist = GeoP.KeyAndDistance + final val GeoKeyAndHash = GeoP.KeyAndHash + final val GeoKeyCoordAndDist = GeoP.KeyCoordinatesAndDistance + final val GeoKeyCoordAndHash = GeoP.KeyCoordinatesAndHash + final val GeoKeyDistAndHash = GeoP.KeyDistanceAndHash + final val GeoKeyCoordDistAndHash = GeoP.KeyCoordinatesDistanceAndHash + final val GeoPosition = GeoP.Position + final val GeoRadiusMode = GeoP.RadiusMode + final val GeoStoreBoth = GeoP.StoreMode.Both + final val GeoStoreDistance = GeoP.StoreMode.Distance + final val GeoStoreHash = GeoP.StoreMode.Hash + final val GeoUnit = GeoP.Unit + } + + import geotypes._ + + final def geoadd(key: Key, positions: OneOrMore[GeoPosition]): Protocol.Aux[NonNegInt] = + Protocol("GEOADD", key :: positions.value.map { case GeoPosition(m, lat, long) => (long -> lat) -> m } :: HNil).as[Num, NonNegInt] + + final def geodist(key: Key, member1: Key, member2: Key): Protocol.Aux[Option[NonNegDouble]] = + Protocol("GEODIST", key :: member1 :: member2 :: HNil).opt[GenBulk].as[NonNegDouble] + final def geodist(key: Key, member1: Key, member2: Key, unit: GeoUnit): Protocol.Aux[Option[NonNegDouble]] = + Protocol("GEODIST", key :: member1 :: member2 :: unit :: HNil).opt[GenBulk].as[NonNegDouble] + + final def geohash(key: Key, members: OneOrMoreKeys): Protocol.Aux[Seq[Option[GeoHash]]] = + Protocol("GEOHASH", key :: members.value).as[Arr, Seq[Option[GeoHash]]] + + final def geopos(key: Key, members: OneOrMoreKeys): Protocol.Aux[Seq[Option[GeoCoordinates]]] = + Protocol("GEOPOS", key :: members.value).as[Arr, Seq[Option[GeoCoordinates]]] + + final def georadius(key: Key, coordinates: GeoCoordinates, radius: NonNegDouble, unit: GeoUnit): Protocol.Aux[Seq[Key]] = + Protocol("GEORADIUS", key :: coordinates.longitude :: coordinates.latitude :: radius :: unit :: HNil).as[Arr, Seq[Key]] + final def georadius(key: Key, coordinates: GeoCoordinates, radius: NonNegDouble, unit: GeoUnit, limit: PosInt): Protocol.Aux[Seq[Key]] = + Protocol("GEORADIUS", key :: coordinates.longitude :: coordinates.latitude :: radius :: unit :: "COUNT" :: limit :: HNil) + .as[Arr, Seq[Key]] + final def georadius(key: Key, coordinates: GeoCoordinates, radius: NonNegDouble, unit: GeoUnit, sort: Direction): Protocol.Aux[Seq[Key]] = + Protocol("GEORADIUS", key :: coordinates.longitude :: coordinates.latitude :: radius :: unit :: sort :: HNil).as[Arr, Seq[Key]] + final def georadius( + key: Key, + coordinates: GeoCoordinates, + radius: NonNegDouble, + unit: GeoUnit, + limit: PosInt, + sort: Direction + ): Protocol.Aux[Seq[Key]] = + Protocol("GEORADIUS", key :: coordinates.longitude :: coordinates.latitude :: radius :: unit :: "COUNT" :: limit :: sort :: HNil) + .as[Arr, Seq[Key]] + final def georadius(key: Key, coordinates: GeoCoordinates, radius: NonNegDouble, unit: GeoUnit, mode: GeoRadiusMode)( + implicit ev: Arr ==> mode.Res + ): Protocol.Aux[Seq[mode.Res]] = + Protocol("GEORADIUS", key :: coordinates.longitude :: coordinates.latitude :: radius :: unit :: mode.params :: HNil) + .as[Arr, Seq[mode.Res]] + final def georadius(key: Key, coordinates: GeoCoordinates, radius: NonNegDouble, unit: GeoUnit, limit: PosInt, mode: GeoRadiusMode)( + implicit ev: Arr ==> mode.Res + ): Protocol.Aux[Seq[mode.Res]] = + Protocol("GEORADIUS", key :: coordinates.longitude :: coordinates.latitude :: radius :: unit :: "COUNT" :: limit :: mode.params :: HNil) + .as[Arr, Seq[mode.Res]] + final def georadius(key: Key, coordinates: GeoCoordinates, radius: NonNegDouble, unit: GeoUnit, sort: Direction, mode: GeoRadiusMode)( + implicit ev: Arr ==> mode.Res + ): Protocol.Aux[Seq[mode.Res]] = + Protocol("GEORADIUS", key :: coordinates.longitude :: coordinates.latitude :: radius :: unit :: sort :: mode.params :: HNil) + .as[Arr, Seq[mode.Res]] + final def georadius( + key: Key, + coordinates: GeoCoordinates, + radius: NonNegDouble, + unit: GeoUnit, + limit: PosInt, + sort: Direction, + mode: GeoRadiusMode + )(implicit ev: Arr ==> mode.Res): Protocol.Aux[Seq[mode.Res]] = + Protocol( + "GEORADIUS", + key :: coordinates.longitude :: coordinates.latitude :: radius :: unit :: "COUNT" :: limit :: sort :: mode.params :: HNil + ).as[Arr, Seq[mode.Res]] + final def georadius( + key: Key, + coordinates: GeoCoordinates, + radius: NonNegDouble, + unit: GeoUnit, + store: GeoStoreMode + ): Protocol.Aux[NonNegInt] = + Protocol("GEORADIUS", key :: coordinates.longitude :: coordinates.latitude :: radius :: unit :: store.params :: HNil).as[Num, NonNegInt] + final def georadius( + key: Key, + coordinates: GeoCoordinates, + radius: NonNegDouble, + unit: GeoUnit, + limit: PosInt, + store: GeoStoreMode + ): Protocol.Aux[NonNegInt] = + Protocol( + "GEORADIUS", + key :: coordinates.longitude :: coordinates.latitude :: radius :: unit :: "COUNT" :: limit :: store.params :: HNil + ).as[Num, NonNegInt] + final def georadius( + key: Key, + coordinates: GeoCoordinates, + radius: NonNegDouble, + unit: GeoUnit, + sort: Direction, + store: GeoStoreMode + ): Protocol.Aux[NonNegInt] = + Protocol("GEORADIUS", key :: coordinates.longitude :: coordinates.latitude :: radius :: unit :: sort :: store.params :: HNil) + .as[Num, NonNegInt] + final def georadius( + key: Key, + coordinates: GeoCoordinates, + radius: NonNegDouble, + unit: GeoUnit, + limit: PosInt, + sort: Direction, + store: GeoStoreMode + ): Protocol.Aux[NonNegInt] = + Protocol( + "GEORADIUS", + key :: coordinates.longitude :: coordinates.latitude :: radius :: unit :: "COUNT" :: limit :: sort :: store.params :: HNil + ).as[Num, NonNegInt] + final def georadius(key: Key, member: Key, radius: NonNegDouble, unit: GeoUnit): Protocol.Aux[Seq[Key]] = + Protocol("GEORADIUSBYMEMBER", key :: member :: radius :: unit :: HNil).as[Arr, Seq[Key]] + final def georadius(key: Key, member: Key, radius: NonNegDouble, unit: GeoUnit, limit: PosInt): Protocol.Aux[Seq[Key]] = + Protocol("GEORADIUSBYMEMBER", key :: member :: radius :: unit :: "COUNT" :: limit :: HNil).as[Arr, Seq[Key]] + final def georadius(key: Key, member: Key, radius: NonNegDouble, unit: GeoUnit, sort: Direction): Protocol.Aux[Seq[Key]] = + Protocol("GEORADIUSBYMEMBER", key :: member :: radius :: unit :: sort :: HNil).as[Arr, Seq[Key]] + final def georadius(key: Key, member: Key, radius: NonNegDouble, unit: GeoUnit, limit: PosInt, sort: Direction): Protocol.Aux[Seq[Key]] = + Protocol("GEORADIUSBYMEMBER", key :: member :: radius :: unit :: "COUNT" :: limit :: sort :: HNil).as[Arr, Seq[Key]] + final def georadius(key: Key, member: Key, radius: NonNegDouble, unit: GeoUnit, mode: GeoRadiusMode)( + implicit ev: Arr ==> mode.Res + ): Protocol.Aux[Seq[mode.Res]] = + Protocol("GEORADIUSBYMEMBER", key :: member :: radius :: unit :: mode.params :: HNil).as[Arr, Seq[mode.Res]] + final def georadius(key: Key, member: Key, radius: NonNegDouble, unit: GeoUnit, limit: PosInt, mode: GeoRadiusMode)( + implicit ev: Arr ==> mode.Res + ): Protocol.Aux[Seq[mode.Res]] = + Protocol("GEORADIUSBYMEMBER", key :: member :: radius :: unit :: "COUNT" :: limit :: mode.params :: HNil).as[Arr, Seq[mode.Res]] + final def georadius(key: Key, member: Key, radius: NonNegDouble, unit: GeoUnit, sort: Direction, mode: GeoRadiusMode)( + implicit ev: Arr ==> mode.Res + ): Protocol.Aux[Seq[mode.Res]] = + Protocol("GEORADIUSBYMEMBER", key :: member :: radius :: unit :: sort :: mode.params :: HNil).as[Arr, Seq[mode.Res]] + final def georadius(key: Key, member: Key, radius: NonNegDouble, unit: GeoUnit, limit: PosInt, sort: Direction, mode: GeoRadiusMode)( + implicit ev: Arr ==> mode.Res + ): Protocol.Aux[Seq[mode.Res]] = + Protocol("GEORADIUSBYMEMBER", key :: member :: radius :: unit :: "COUNT" :: limit :: sort :: mode.params :: HNil).as[Arr, Seq[mode.Res]] + final def georadius(key: Key, member: Key, radius: NonNegDouble, unit: GeoUnit, store: GeoStoreMode): Protocol.Aux[NonNegInt] = + Protocol("GEORADIUSBYMEMBER", key :: member :: radius :: unit :: store.params :: HNil).as[Num, NonNegInt] + final def georadius( + key: Key, + member: Key, + radius: NonNegDouble, + unit: GeoUnit, + limit: PosInt, + store: GeoStoreMode + ): Protocol.Aux[NonNegInt] = + Protocol("GEORADIUSBYMEMBER", key :: member :: radius :: unit :: "COUNT" :: limit :: store.params :: HNil).as[Num, NonNegInt] + final def georadius( + key: Key, + member: Key, + radius: NonNegDouble, + unit: GeoUnit, + sort: Direction, + store: GeoStoreMode + ): Protocol.Aux[NonNegInt] = + Protocol("GEORADIUSBYMEMBER", key :: member :: radius :: unit :: sort :: store.params :: HNil).as[Num, NonNegInt] + final def georadius( + key: Key, + member: Key, + radius: NonNegDouble, + unit: GeoUnit, + limit: PosInt, + sort: Direction, + store: GeoStoreMode + ): Protocol.Aux[NonNegInt] = + Protocol("GEORADIUSBYMEMBER", key :: member :: radius :: unit :: "COUNT" :: limit :: sort :: store.params :: HNil).as[Num, NonNegInt] + + final object ro { + final def georadius(key: Key, coordinates: GeoCoordinates, radius: NonNegDouble, unit: GeoUnit): Protocol.Aux[Seq[Key]] = + Protocol("GEORADIUS_RO", key :: coordinates.longitude :: coordinates.latitude :: radius :: unit :: HNil).as[Arr, Seq[Key]] + final def georadius(key: Key, coordinates: GeoCoordinates, radius: NonNegDouble, unit: GeoUnit, limit: PosInt): Protocol.Aux[Seq[Key]] = + Protocol("GEORADIUS_RO", key :: coordinates.longitude :: coordinates.latitude :: radius :: unit :: "COUNT" :: limit :: HNil) + .as[Arr, Seq[Key]] + final def georadius( + key: Key, + coordinates: GeoCoordinates, + radius: NonNegDouble, + unit: GeoUnit, + sort: Direction + ): Protocol.Aux[Seq[Key]] = + Protocol("GEORADIUS_RO", key :: coordinates.longitude :: coordinates.latitude :: radius :: unit :: sort :: HNil).as[Arr, Seq[Key]] + final def georadius( + key: Key, + coordinates: GeoCoordinates, + radius: NonNegDouble, + unit: GeoUnit, + limit: PosInt, + sort: Direction + ): Protocol.Aux[Seq[Key]] = + Protocol("GEORADIUS_RO", key :: coordinates.longitude :: coordinates.latitude :: radius :: unit :: "COUNT" :: limit :: sort :: HNil) + .as[Arr, Seq[Key]] + final def georadius(key: Key, coordinates: GeoCoordinates, radius: NonNegDouble, unit: GeoUnit, mode: GeoRadiusMode)( + implicit ev: Arr ==> mode.Res + ): Protocol.Aux[Seq[mode.Res]] = + Protocol("GEORADIUS_RO", key :: coordinates.longitude :: coordinates.latitude :: radius :: unit :: mode.params :: HNil) + .as[Arr, Seq[mode.Res]] + final def georadius(key: Key, coordinates: GeoCoordinates, radius: NonNegDouble, unit: GeoUnit, limit: PosInt, mode: GeoRadiusMode)( + implicit ev: Arr ==> mode.Res + ): Protocol.Aux[Seq[mode.Res]] = + Protocol( + "GEORADIUS_RO", + key :: coordinates.longitude :: coordinates.latitude :: radius :: unit :: "COUNT" :: limit :: mode.params :: HNil + ).as[Arr, Seq[mode.Res]] + final def georadius(key: Key, coordinates: GeoCoordinates, radius: NonNegDouble, unit: GeoUnit, sort: Direction, mode: GeoRadiusMode)( + implicit ev: Arr ==> mode.Res + ): Protocol.Aux[Seq[mode.Res]] = + Protocol("GEORADIUS_RO", key :: coordinates.longitude :: coordinates.latitude :: radius :: unit :: sort :: mode.params :: HNil) + .as[Arr, Seq[mode.Res]] + final def georadius( + key: Key, + coordinates: GeoCoordinates, + radius: NonNegDouble, + unit: GeoUnit, + limit: PosInt, + sort: Direction, + mode: GeoRadiusMode + )(implicit ev: Arr ==> mode.Res): Protocol.Aux[Seq[mode.Res]] = + Protocol( + "GEORADIUS_RO", + key :: coordinates.longitude :: coordinates.latitude :: radius :: unit :: "COUNT" :: limit :: sort :: mode.params :: HNil + ).as[Arr, Seq[mode.Res]] + final def georadius(key: Key, member: Key, radius: NonNegDouble, unit: GeoUnit): Protocol.Aux[Seq[Key]] = + Protocol("GEORADIUSBYMEMBER_RO", key :: member :: radius :: unit :: HNil).as[Arr, Seq[Key]] + final def georadius(key: Key, member: Key, radius: NonNegDouble, unit: GeoUnit, limit: PosInt): Protocol.Aux[Seq[Key]] = + Protocol("GEORADIUSBYMEMBER_RO", key :: member :: radius :: unit :: "COUNT" :: limit :: HNil).as[Arr, Seq[Key]] + final def georadius(key: Key, member: Key, radius: NonNegDouble, unit: GeoUnit, sort: Direction): Protocol.Aux[Seq[Key]] = + Protocol("GEORADIUSBYMEMBER_RO", key :: member :: radius :: unit :: sort :: HNil).as[Arr, Seq[Key]] + final def georadius( + key: Key, + member: Key, + radius: NonNegDouble, + unit: GeoUnit, + limit: PosInt, + sort: Direction + ): Protocol.Aux[Seq[Key]] = + Protocol("GEORADIUSBYMEMBER_RO", key :: member :: radius :: unit :: "COUNT" :: limit :: sort :: HNil).as[Arr, Seq[Key]] + final def georadius(key: Key, member: Key, radius: NonNegDouble, unit: GeoUnit, mode: GeoRadiusMode)( + implicit ev: Arr ==> mode.Res + ): Protocol.Aux[Seq[mode.Res]] = + Protocol("GEORADIUSBYMEMBER_RO", key :: member :: radius :: unit :: mode.params :: HNil).as[Arr, Seq[mode.Res]] + final def georadius(key: Key, member: Key, radius: NonNegDouble, unit: GeoUnit, limit: PosInt, mode: GeoRadiusMode)( + implicit ev: Arr ==> mode.Res + ): Protocol.Aux[Seq[mode.Res]] = + Protocol("GEORADIUSBYMEMBER_RO", key :: member :: radius :: unit :: "COUNT" :: limit :: mode.params :: HNil).as[Arr, Seq[mode.Res]] + final def georadius(key: Key, member: Key, radius: NonNegDouble, unit: GeoUnit, sort: Direction, mode: GeoRadiusMode)( + implicit ev: Arr ==> mode.Res + ): Protocol.Aux[Seq[mode.Res]] = + Protocol("GEORADIUSBYMEMBER_RO", key :: member :: radius :: unit :: sort :: mode.params :: HNil).as[Arr, Seq[mode.Res]] + final def georadius(key: Key, member: Key, radius: NonNegDouble, unit: GeoUnit, limit: PosInt, sort: Direction, mode: GeoRadiusMode)( + implicit ev: Arr ==> mode.Res + ): Protocol.Aux[Seq[mode.Res]] = + Protocol("GEORADIUSBYMEMBER_RO", key :: member :: radius :: unit :: "COUNT" :: limit :: sort :: mode.params :: HNil) + .as[Arr, Seq[mode.Res]] + } +} + +trait GeoP extends GeoBaseP with GeoExtP diff --git a/core/src/main/scala/laserdisc/protocol/HashP.scala b/core/src/main/scala/laserdisc/protocol/HashP.scala index 1bd54378..79ad74f8 100644 --- a/core/src/main/scala/laserdisc/protocol/HashP.scala +++ b/core/src/main/scala/laserdisc/protocol/HashP.scala @@ -1,81 +1,63 @@ package laserdisc package protocol -trait HashP { - import Read.==> +trait HashBaseP { import shapeless._ import shapeless.labelled.FieldType import shapeless.nat._1 import shapeless.ops.hlist.Length import shapeless.ops.nat.GTEq.>= - final def hdel(key: Key, fields: OneOrMoreKeys): Protocol.Aux[NonNegInt] = - Protocol("HDEL", key :: fields.value).as[Integer, NonNegInt] + final def hdel(key: Key, fields: OneOrMoreKeys): Protocol.Aux[NonNegInt] = Protocol("HDEL", key :: fields.value).as[Num, NonNegInt] - final def hexists(key: Key, field: Key): Protocol.Aux[Boolean] = - Protocol("HEXISTS", key :: field :: Nil).as[Integer, Boolean] + final def hexists(key: Key, field: Key): Protocol.Aux[Boolean] = Protocol("HEXISTS", key :: field :: Nil).as[Num, Boolean] - final def hget[A](key: Key, field: Key)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Option[A]] = - Protocol("HGET", key :: field :: Nil).asC[NullBulkString :+: NonNullBulkString :+: CNil, Option[A]] + final def hget[A: Bulk ==> ?](key: Key, field: Key): Protocol.Aux[Option[A]] = + Protocol("HGET", key :: field :: Nil).opt[GenBulk].as[A] - final def hgetall[A](key: Key)( - implicit ev: NonNilArray ==> A - ): Protocol.Aux[A] = Protocol("HGETALL", key).as[NonNilArray, A] + final def hgetall[A: Arr ==> ?](key: Key): Protocol.Aux[A] = Protocol("HGETALL", key).as[Arr, A] final def hincrby(key: Key, field: Key, increment: NonZeroLong): Protocol.Aux[Long] = - Protocol("HINCRBY", key :: field :: increment :: HNil).as[Integer, Long] + Protocol("HINCRBY", key :: field :: increment :: HNil).as[Num, Long] + final def hincrby(key: Key, field: Key, increment: NonZeroDouble): Protocol.Aux[Double] = + Protocol("HINCRBYFLOAT", key :: field :: increment :: HNil).as[Bulk, Double] - final def hincrbyfloat(key: Key, field: Key, increment: NonZeroDouble): Protocol.Aux[Double] = - Protocol("HINCRBYFLOAT", key :: field :: increment :: HNil).as[NonNullBulkString, Double] + final def hkeys(key: Key): Protocol.Aux[Seq[Key]] = Protocol("HKEYS", key).as[Arr, Seq[Key]] - final def hkeys(key: Key): Protocol.Aux[Seq[Key]] = Protocol("HKEYS", key).as[NonNilArray, Seq[Key]] + final def hlen(key: Key): Protocol.Aux[NonNegInt] = Protocol("HLEN", key).as[Num, NonNegInt] - final def hlen(key: Key): Protocol.Aux[NonNegInt] = Protocol("HLEN", key).as[Integer, NonNegInt] + final def hmget[L <: HList: Arr ==> ?](key: Key, fields: OneOrMoreKeys): Protocol.Aux[L] = + Protocol("HMGET", key :: fields.value).as[Arr, L] - final def hmget[L <: HList](key: Key, fields: OneOrMoreKeys)( - implicit ev: NonNilArray ==> L - ): Protocol.Aux[L] = Protocol("HMGET", key :: fields.value).as[NonNilArray, L] - - final def hmset[L <: HList: RESPParamWrite, N <: Nat](key: Key, l: L)( + final def hmset[L <: HList: RESPParamWrite: LUBConstraint[?, (Key, _)], N <: Nat](key: Key, l: L)( implicit ev0: Length.Aux[L, N], - ev1: N >= _1, - ev2: LUBConstraint[L, (Key, _)] - ): Protocol.Aux[OK] = Protocol("HMSET", key :: l).as[SimpleString, OK] - + ev1: N >= _1 + ): Protocol.Aux[OK] = Protocol("HMSET", key :: l).as[Str, OK] final def hmset[P <: Product, L <: HList, N <: Nat](key: Key, product: P)( implicit gen: LabelledGeneric.Aux[P, L], ev0: Length.Aux[L, N], ev1: N >= _1, ev2: LUBConstraint[L, FieldType[_, _]], ev3: RESPParamWrite[L] - ): Protocol.Aux[OK] = Protocol("HMSET", key :: gen.to(product)).as[SimpleString, OK] - - final def hscan(key: Key, cursor: NonNegLong): Protocol.Aux[ScanKV] = - Protocol("HSCAN", key :: cursor :: HNil).as[NonNilArray, ScanKV] + ): Protocol.Aux[OK] = Protocol("HMSET", key :: gen.to(product)).as[Str, OK] + final def hscan(key: Key, cursor: NonNegLong): Protocol.Aux[ScanKV] = Protocol("HSCAN", key :: cursor :: HNil).as[Arr, ScanKV] final def hscan(key: Key, cursor: NonNegLong, pattern: GlobPattern): Protocol.Aux[ScanKV] = - Protocol("HSCAN", key :: cursor :: "MATCH" :: pattern :: HNil).as[NonNilArray, ScanKV] - + Protocol("HSCAN", key :: cursor :: "MATCH" :: pattern :: HNil).as[Arr, ScanKV] final def hscan(key: Key, cursor: NonNegLong, count: PosInt): Protocol.Aux[ScanKV] = - Protocol("HSCAN", key :: cursor :: "COUNT" :: count :: HNil).as[NonNilArray, ScanKV] - + Protocol("HSCAN", key :: cursor :: "COUNT" :: count :: HNil).as[Arr, ScanKV] final def hscan(key: Key, cursor: NonNegLong, pattern: GlobPattern, count: PosInt): Protocol.Aux[ScanKV] = - Protocol("HSCAN", key :: cursor :: "MATCH" :: pattern :: "COUNT" :: count :: HNil).as[NonNilArray, ScanKV] + Protocol("HSCAN", key :: cursor :: "MATCH" :: pattern :: "COUNT" :: count :: HNil).as[Arr, ScanKV] final def hset[A: Show](key: Key, field: Key, value: A): Protocol.Aux[Boolean] = - Protocol("HSET", key :: field :: value :: HNil).as[Integer, Boolean] + Protocol("HSET", key :: field :: value :: HNil).as[Num, Boolean] final def hsetnx[A: Show](key: Key, field: Key, value: A): Protocol.Aux[Boolean] = - Protocol("HSETNX", key :: field :: value :: HNil).as[Integer, Boolean] + Protocol("HSETNX", key :: field :: value :: HNil).as[Num, Boolean] - final def hstrlen(key: Key, field: Key): Protocol.Aux[NonNegInt] = - Protocol("HSTRLEN", key :: field :: Nil).as[Integer, NonNegInt] + final def hstrlen(key: Key, field: Key): Protocol.Aux[NonNegInt] = Protocol("HSTRLEN", key :: field :: Nil).as[Num, NonNegInt] - final def hvals[L <: HList](key: Key)( - implicit ev: NonNilArray ==> L - ): Protocol.Aux[L] = Protocol("HVALS", key).as[NonNilArray, L] + final def hvals[L <: HList: Arr ==> ?](key: Key): Protocol.Aux[L] = Protocol("HVALS", key).as[Arr, L] } -trait AllHashP extends HashP with HashPExtra +trait HashP extends HashBaseP with HashExtP diff --git a/core/src/main/scala/laserdisc/protocol/HyperLogLogP.scala b/core/src/main/scala/laserdisc/protocol/HyperLogLogP.scala index 42ffddcb..d487c254 100644 --- a/core/src/main/scala/laserdisc/protocol/HyperLogLogP.scala +++ b/core/src/main/scala/laserdisc/protocol/HyperLogLogP.scala @@ -1,15 +1,13 @@ package laserdisc package protocol -trait HyperLogLogP { - final def pfadd(key: Key, elements: OneOrMoreKeys): Protocol.Aux[Boolean] = - Protocol("PFADD", key :: elements.value).as[Integer, Boolean] +trait HyperLogLogBaseP { + final def pfadd(key: Key, elements: OneOrMoreKeys): Protocol.Aux[Boolean] = Protocol("PFADD", key :: elements.value).as[Num, Boolean] - final def pfcount(keys: OneOrMoreKeys): Protocol.Aux[NonNegInt] = - Protocol("PFCOUNT", keys.value).as[Integer, NonNegInt] + final def pfcount(keys: OneOrMoreKeys): Protocol.Aux[NonNegInt] = Protocol("PFCOUNT", keys.value).as[Num, NonNegInt] final def pfmerge(sourceKeys: TwoOrMoreKeys, destinationKey: Key): Protocol.Aux[OK] = - Protocol("PFMERGE", destinationKey :: sourceKeys.value).as[SimpleString, OK] + Protocol("PFMERGE", destinationKey :: sourceKeys.value).as[Str, OK] } -trait AllHyperLogLogP extends HyperLogLogP with HyperLogLogPExtra +trait HyperLogLogP extends HyperLogLogBaseP with HyperLogLogExtP diff --git a/core/src/main/scala/laserdisc/protocol/KeyP.scala b/core/src/main/scala/laserdisc/protocol/KeyP.scala index 63a99ce7..5de6f7ac 100644 --- a/core/src/main/scala/laserdisc/protocol/KeyP.scala +++ b/core/src/main/scala/laserdisc/protocol/KeyP.scala @@ -2,21 +2,8 @@ package laserdisc package protocol object KeyP { - import Read.==> - - sealed trait Direction - final object Direction { - final object asc extends Direction - final object desc extends Direction - - implicit val directionShow: Show[Direction] = Show.instance { - case `asc` => "ASC" - case `desc` => "DESC" - } - } - sealed trait Encoding - object Encoding { + final object Encoding { final case object raw extends Encoding final case object int extends Encoding final case object ziplist extends Encoding @@ -25,19 +12,43 @@ object KeyP { final case object hashtable extends Encoding final case object skiplist extends Encoding - implicit final val nonEmptyBulkString2EncodingRead: NonNullBulkString ==> Encoding = Read.instancePF { - case NonNullBulkString("raw") => raw - case NonNullBulkString("int") => int - case NonNullBulkString("ziplist") => ziplist - case NonNullBulkString("linkedlist") => linkedlist - case NonNullBulkString("intset") => intset - case NonNullBulkString("hashtable") => hashtable - case NonNullBulkString("skiplist") => skiplist + implicit final val bulk2EncodingRead: Bulk ==> Encoding = Read.instancePF { + case Bulk("raw") => raw + case Bulk("int") => int + case Bulk("ziplist") => ziplist + case Bulk("linkedlist") => linkedlist + case Bulk("intset") => intset + case Bulk("hashtable") => hashtable + case Bulk("skiplist") => skiplist } } + sealed trait MigrateMode { def params: List[String] } + final object MigrateMode { + final object copy extends MigrateMode { override final val params: List[String] = List("COPY") } + final object replace extends MigrateMode { override final val params: List[String] = List("REPLACE") } + final object both extends MigrateMode { override final val params: List[String] = List("COPY", "REPLACE") } + } + + sealed trait RestoreEviction { def param: String; def seconds: NonNegInt } + final object RestoreEviction { + final case class IdleTime(override final val seconds: NonNegInt) extends RestoreEviction { + override final val param: String = "IDLETIME" + } + final case class Frequency(override final val seconds: NonNegInt) extends RestoreEviction { + override final val param: String = "FREQUENCY" + } + } + + sealed trait RestoreMode { def params: List[String] } + final object RestoreMode { + final case object replace extends RestoreMode { override final val params: List[String] = List("REPLACE") } + final case object absolutettl extends RestoreMode { override final val params: List[String] = List("ABSTTL") } + final case object both extends RestoreMode { override final val params: List[String] = List("REPLACE", "ABSTTL") } + } + sealed trait Type - object Type { + final object Type { final case object string extends Type final case object list extends Type final case object set extends Type @@ -51,140 +62,164 @@ object KeyP { final case object NoExpire extends TTLResponse final case class ExpireAfter(ttl: NonNegLong) extends TTLResponse - implicit final val integer2TTLResponseRead: Integer ==> TTLResponse = Read.instancePF { - case Integer(-2) => NoKey - case Integer(-1) => NoExpire - case Integer(l) if l >= 0 => ExpireAfter(NonNegLong.unsafeFrom(l)) + implicit final val num2TTLResponseRead: Num ==> TTLResponse = Read.instancePF { + case Num(-2) => NoKey + case Num(-1) => NoExpire + case Num(l) if l >= 0 => ExpireAfter(NonNegLong.unsafeFrom(l)) } } } -trait KeyP { - import KeyP.{Direction, Encoding, Type, TTLResponse} - import Read.==> +trait KeyBaseP { import shapeless._ - private[this] final val zeroIsNone = RESPRead.instance(Read.integerZeroIsNone[PosInt]) + final object keytypes { + final type KeyEncoding = KeyP.Encoding + final type KeyMigrateMode = KeyP.MigrateMode + final type KeyRestoreEviction = KeyP.RestoreEviction + final type KeyRestoreMode = KeyP.RestoreMode + final type KeyType = KeyP.Type + final type KeyTTLResponse = KeyP.TTLResponse + + final val KeyEncoding = KeyP.Encoding + final val KeyMigrateMode = KeyP.MigrateMode + final val KeyIdleTimeEviction = KeyP.RestoreEviction.IdleTime + final val KeyFrequencyEviction = KeyP.RestoreEviction.Frequency + final val KeyRestoreMode = KeyP.RestoreMode + final val KeyType = KeyP.Type + final val KeyNoKeyTTLResponse = KeyP.TTLResponse.NoKey + final val KeyNoExpireTTLResponse = KeyP.TTLResponse.NoExpire + final val KeyExpireAfterTTLResponse = KeyP.TTLResponse.ExpireAfter + } + + import keytypes._ - private[this] implicit final val simpleString2OptionType: SimpleString ==> Option[Type] = Read.instancePF { - case SimpleString("none") => None - case SimpleString("string") => Some(Type.string) - case SimpleString("list") => Some(Type.list) - case SimpleString("set") => Some(Type.set) - case SimpleString("zset") => Some(Type.zset) - case SimpleString("hash") => Some(Type.hash) + private[this] implicit final val str2NOKEYOrOK: Str ==> (NOKEY | OK) = Read.instancePF { + case Str("NOKEY") => Left(NOKEY) + case Str("OK") => Right(OK) } - final object keys { - final val direction = Direction + private[this] final val zeroIsNone = RESPRead.instance(Read.numZeroIsNone[PosInt]) + + private[this] implicit final val str2OptionType: Str ==> Option[KeyType] = Read.instancePF { + case Str("none") => None + case Str("string") => Some(KeyType.string) + case Str("list") => Some(KeyType.list) + case Str("set") => Some(KeyType.set) + case Str("zset") => Some(KeyType.zset) + case Str("hash") => Some(KeyType.hash) } - final def del(keys: OneOrMoreKeys): Protocol.Aux[NonNegInt] = Protocol("DEL", keys.value).as[Integer, NonNegInt] + final def del(keys: OneOrMoreKeys): Protocol.Aux[NonNegInt] = Protocol("DEL", keys.value).as[Num, NonNegInt] - final def dump(key: Key): Protocol.Aux[Option[NonNullBulkString]] = - Protocol("DUMP", key).asC[NullBulkString :+: NonNullBulkString :+: CNil, Option[NonNullBulkString]] + final def dump(key: Key): Protocol.Aux[Option[Bulk]] = Protocol("DUMP", key).opt[GenBulk].as[Bulk] final def exists(keys: OneOrMoreKeys): Protocol.Aux[Option[PosInt]] = Protocol("EXISTS", keys.value).using(zeroIsNone) //TODO check if we must support deletions via timeout < 0 - final def expire(key: Key, seconds: NonNegInt): Protocol.Aux[Boolean] = - Protocol("EXPIRE", key :: seconds :: HNil).as[Integer, Boolean] - - final def expireat(key: Key, seconds: NonNegInt): Protocol.Aux[Boolean] = - Protocol("EXPIREAT", key :: seconds :: HNil).as[Integer, Boolean] - - final def keys(pattern: GlobPattern): Protocol.Aux[Seq[Key]] = - Protocol("KEYS", pattern :: HNil).as[NonNilArray, Seq[Key]] - - //FIXME add migrate - - final def move(key: Key, db: DbIndex): Protocol.Aux[Boolean] = - Protocol("MOVE", key :: db :: HNil).as[Integer, Boolean] + final def expire(key: Key, seconds: NonNegInt): Protocol.Aux[Boolean] = Protocol("EXPIRE", key :: seconds :: HNil).as[Num, Boolean] + + final def expireat(key: Key, seconds: NonNegInt): Protocol.Aux[Boolean] = Protocol("EXPIREAT", key :: seconds :: HNil).as[Num, Boolean] + + final def keys(pattern: GlobPattern): Protocol.Aux[Seq[Key]] = Protocol("KEYS", pattern :: HNil).as[Arr, Seq[Key]] + + final def migrate(key: Key, host: Host, port: Port, dbIndex: DbIndex, timeout: NonNegInt): Protocol.Aux[NOKEY | OK] = + Protocol("MIGRATE", host :: port :: key :: dbIndex :: timeout :: HNil).as[Str, NOKEY | OK] + final def migrate(keys: TwoOrMoreKeys, host: Host, port: Port, dbIndex: DbIndex, timeout: NonNegInt): Protocol.Aux[NOKEY | OK] = + Protocol("MIGRATE", host :: port :: "" :: dbIndex :: timeout :: "KEYS" :: keys.value :: HNil).as[Str, NOKEY | OK] + final def migrate( + key: Key, + host: Host, + port: Port, + dbIndex: DbIndex, + timeout: NonNegInt, + mode: KeyMigrateMode + ): Protocol.Aux[NOKEY | OK] = Protocol("MIGRATE", host :: port :: key :: dbIndex :: timeout :: mode.params :: HNil).as[Str, NOKEY | OK] + final def migrate( + keys: TwoOrMoreKeys, + host: Host, + port: Port, + dbIndex: DbIndex, + timeout: NonNegInt, + mode: KeyMigrateMode + ): Protocol.Aux[NOKEY | OK] = + Protocol("MIGRATE", host :: port :: "" :: dbIndex :: timeout :: mode.params :: "KEYS" :: keys.value :: HNil) + .as[Str, NOKEY | OK] + + final def move(key: Key, db: DbIndex): Protocol.Aux[Boolean] = Protocol("MOVE", key :: db :: HNil).as[Num, Boolean] final object obj { - def refcount(key: Key): Protocol.Aux[NonNegInt] = - Protocol("OBJECT", "REFCOUNT" :: key.value :: Nil).as[Integer, NonNegInt] + def encoding(key: Key): Protocol.Aux[Option[KeyEncoding]] = + Protocol("OBJECT", "ENCODING" :: key.value :: Nil).opt[GenBulk].as[KeyEncoding] - def encoding(key: Key): Protocol.Aux[Option[Encoding]] = - Protocol("OBJECT", "ENCODING" :: key.value :: Nil) - .asC[NullBulkString :+: NonNullBulkString :+: CNil, Option[Encoding]] + def freq(key: Key): Protocol.Aux[NonNegInt] = Protocol("OBJECT", "FREQ" :: key.value :: Nil).as[Num, NonNegInt] - //FIXME add freq + def idletime(key: Key): Protocol.Aux[NonNegInt] = Protocol("OBJECT", "IDLETIME" :: key.value :: Nil).as[Num, NonNegInt] - def idletime(key: Key): Protocol.Aux[Option[NonNegInt]] = - Protocol("OBJECT", "IDLETIME" :: key.value :: Nil).asC[NullBulkString :+: Integer :+: CNil, Option[NonNegInt]] + def refcount(key: Key): Protocol.Aux[NonNegInt] = Protocol("OBJECT", "REFCOUNT" :: key.value :: Nil).as[Num, NonNegInt] } - final def persist(key: Key): Protocol.Aux[Boolean] = Protocol("PERSIST", key).as[Integer, Boolean] + final def persist(key: Key): Protocol.Aux[Boolean] = Protocol("PERSIST", key).as[Num, Boolean] //TODO check if we must support deletions via timeout < 0 final def pexpire(key: Key, milliseconds: NonNegLong): Protocol.Aux[Boolean] = - Protocol("PEXPIRE", key :: milliseconds :: HNil).as[Integer, Boolean] + Protocol("PEXPIRE", key :: milliseconds :: HNil).as[Num, Boolean] final def pexpireat(key: Key, millisecondsTimestamp: NonNegLong): Protocol.Aux[Boolean] = - Protocol("PEXPIREAT", key :: millisecondsTimestamp :: HNil).as[Integer, Boolean] - - final def pttl(key: Key): Protocol.Aux[TTLResponse] = Protocol("PTTL", key).as[Integer, TTLResponse] - - final val randomKey: Protocol.Aux[Option[Key]] = - Protocol("RANDOMKEY", Nil).asC[NullBulkString :+: NonNullBulkString :+: CNil, Option[Key]] + Protocol("PEXPIREAT", key :: millisecondsTimestamp :: HNil).as[Num, Boolean] - final def rename(key: Key, newKey: Key): Protocol.Aux[Key] = - Protocol("RENAME", key :: newKey :: Nil).as[SimpleString, Key] + final def pttl(key: Key): Protocol.Aux[KeyTTLResponse] = Protocol("PTTL", key).as[Num, KeyTTLResponse] - final def renamenx(key: Key, newKey: Key): Protocol.Aux[Boolean] = - Protocol("RENAMENX", key :: newKey :: Nil).as[Integer, Boolean] + final val randomkey: Protocol.Aux[Option[Key]] = Protocol("RANDOMKEY", Nil).opt[GenBulk].as[Key] - final def restore(key: Key, ttl: NonNegLong, serializedValue: NonNullBulkString): Protocol.Aux[OK] = - Protocol("RESTORE", key :: ttl :: serializedValue :: HNil).as[SimpleString, OK] + final def rename(key: Key, newKey: Key): Protocol.Aux[OK] = Protocol("RENAME", key :: newKey :: Nil).as[Str, OK] - final def restorereplace(key: Key, ttl: NonNegLong, serializedValue: NonNullBulkString): Protocol.Aux[OK] = - Protocol("RESTORE", key :: ttl :: serializedValue :: "REPLACE" :: HNil).as[SimpleString, OK] + final def renamenx(key: Key, newKey: Key): Protocol.Aux[Boolean] = Protocol("RENAMENX", key :: newKey :: Nil).as[Num, Boolean] - final def scan(cursor: NonNegLong): Protocol.Aux[Scan[Key]] = Protocol("SCAN", cursor).as[NonNilArray, Scan[Key]] + final def restore(key: Key, ttl: NonNegLong, serializedValue: Bulk): Protocol.Aux[OK] = + Protocol("RESTORE", key :: ttl :: serializedValue :: HNil).as[Str, OK] + final def restore(key: Key, ttl: NonNegLong, serializedValue: Bulk, mode: KeyRestoreMode): Protocol.Aux[OK] = + Protocol("RESTORE", key :: ttl :: serializedValue :: mode.params :: HNil).as[Str, OK] + final def restore(key: Key, ttl: NonNegLong, serializedValue: Bulk, eviction: KeyRestoreEviction): Protocol.Aux[OK] = + Protocol("RESTORE", key :: ttl :: serializedValue :: eviction.param :: eviction.seconds :: HNil).as[Str, OK] + final def restore( + key: Key, + ttl: NonNegLong, + serializedValue: Bulk, + mode: KeyRestoreMode, + eviction: KeyRestoreEviction + ): Protocol.Aux[OK] = + Protocol("RESTORE", key :: ttl :: serializedValue :: mode.params :: eviction.param :: eviction.seconds :: HNil).as[Str, OK] + final def scan(cursor: NonNegLong): Protocol.Aux[Scan[Key]] = Protocol("SCAN", cursor).as[Arr, Scan[Key]] final def scan(cursor: NonNegLong, pattern: GlobPattern): Protocol.Aux[Scan[Key]] = - Protocol("SCAN", cursor :: "MATCH" :: pattern :: HNil).as[NonNilArray, Scan[Key]] - + Protocol("SCAN", cursor :: "MATCH" :: pattern :: HNil).as[Arr, Scan[Key]] final def scan(cursor: NonNegLong, count: PosInt): Protocol.Aux[Scan[Key]] = - Protocol("SCAN", cursor :: "COUNT" :: count :: HNil).as[NonNilArray, Scan[Key]] - + Protocol("SCAN", cursor :: "COUNT" :: count :: HNil).as[Arr, Scan[Key]] final def scan(cursor: NonNegLong, pattern: GlobPattern, count: PosInt): Protocol.Aux[Scan[Key]] = - Protocol("SCAN", cursor :: "MATCH" :: pattern :: "COUNT" :: count :: HNil).as[NonNilArray, Scan[Key]] + Protocol("SCAN", cursor :: "MATCH" :: pattern :: "COUNT" :: count :: HNil).as[Arr, Scan[Key]] //FIXME sort has many more combinations - final def sort[A](key: Key)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[A]] = Protocol("SORT", key).as[NonNilArray, Seq[A]] - - final def sort[A](key: Key, pattern: GlobPattern)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[A]] = Protocol("SORT", key :: "BY" :: pattern :: HNil).as[NonNilArray, Seq[A]] - - final def sort[A](key: Key, offset: NonNegLong, count: PosLong)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[A]] = Protocol("SORT", key :: "LIMIT" :: offset :: count :: HNil).as[NonNilArray, Seq[A]] - - final def sort[A](key: Key, direction: Direction)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[A]] = Protocol("SORT", key :: direction :: HNil).as[NonNilArray, Seq[A]] - - final def sortstore(key: Key, destination: Key): Protocol.Aux[NonNegInt] = - Protocol("SORT", key.value :: "STORE" :: destination.value :: Nil).as[Integer, NonNegInt] - - final def touch(keys: OneOrMoreKeys): Protocol.Aux[NonNegInt] = Protocol("TOUCH", keys.value).as[Integer, NonNegInt] + final def sort[A: Bulk ==> ?](key: Key): Protocol.Aux[Seq[A]] = Protocol("SORT", key).as[Arr, Seq[A]] + final def sort[A: Bulk ==> ?](key: Key, pattern: GlobPattern): Protocol.Aux[Seq[A]] = + Protocol("SORT", key :: "BY" :: pattern :: HNil).as[Arr, Seq[A]] + final def sort[A: Bulk ==> ?](key: Key, offset: NonNegLong, count: PosLong): Protocol.Aux[Seq[A]] = + Protocol("SORT", key :: "LIMIT" :: offset :: count :: HNil).as[Arr, Seq[A]] + final def sort[A: Bulk ==> ?](key: Key, direction: Direction): Protocol.Aux[Seq[A]] = + Protocol("SORT", key :: direction :: HNil).as[Arr, Seq[A]] + final def sort(key: Key, destination: Key): Protocol.Aux[NonNegInt] = + Protocol("SORT", key.value :: "STORE" :: destination.value :: Nil).as[Num, NonNegInt] - final def ttl(key: Key): Protocol.Aux[TTLResponse] = Protocol("TTL", key).as[Integer, TTLResponse] + final def touch(keys: OneOrMoreKeys): Protocol.Aux[NonNegInt] = Protocol("TOUCH", keys.value).as[Num, NonNegInt] - final def typeof(key: Key): Protocol.Aux[Option[Type]] = Protocol("TYPE", key).as[SimpleString, Option[Type]] + final def ttl(key: Key): Protocol.Aux[KeyTTLResponse] = Protocol("TTL", key).as[Num, KeyTTLResponse] - final def unlink(keys: OneOrMoreKeys): Protocol.Aux[NonNegInt] = Protocol("UNLINK", keys.value).as[Integer, NonNegInt] + final def typeof(key: Key): Protocol.Aux[Option[KeyType]] = Protocol("TYPE", key).as[Str, Option[KeyType]] - final def waitFor(numSlaves: PosInt): Protocol.Aux[PosInt] = - Protocol("WAIT", numSlaves :: 0 :: HNil).as[Integer, PosInt] + final def unlink(keys: OneOrMoreKeys): Protocol.Aux[NonNegInt] = Protocol("UNLINK", keys.value).as[Num, NonNegInt] - final def waitFor(numSlaves: PosInt, timeout: PosLong): Protocol.Aux[PosInt] = - Protocol("WAIT", numSlaves :: timeout :: HNil).as[Integer, PosInt] + final def wait(replicas: PosInt): Protocol.Aux[PosInt] = Protocol("WAIT", replicas :: 0 :: HNil).as[Num, PosInt] + final def wait(replicas: PosInt, timeout: PosLong): Protocol.Aux[PosInt] = Protocol("WAIT", replicas :: timeout :: HNil).as[Num, PosInt] } -trait AllKeyP extends KeyP with KeyPExtra +trait KeyP extends KeyBaseP with KeyExtP diff --git a/core/src/main/scala/laserdisc/protocol/LenientStringCodec.scala b/core/src/main/scala/laserdisc/protocol/LenientStringCodec.scala index 286fd7dd..5af95524 100644 --- a/core/src/main/scala/laserdisc/protocol/LenientStringCodec.scala +++ b/core/src/main/scala/laserdisc/protocol/LenientStringCodec.scala @@ -5,7 +5,7 @@ import java.nio.charset.Charset import scodec.{Attempt, Codec, DecodeResult, SizeBound} import scodec.bits.{BitVector, ByteVector} -final class LenientStringCodec(charset: Charset) extends Codec[String] { +final class LenientStringCodec(private[this] val charset: Charset) extends Codec[String] { override def sizeBound: SizeBound = SizeBound.unknown override def encode(str: String): Attempt[BitVector] = Attempt.successful(ByteVector.view(str.getBytes(charset)).bits) override def decode(bits: BitVector): Attempt[DecodeResult[String]] = diff --git a/core/src/main/scala/laserdisc/protocol/ListP.scala b/core/src/main/scala/laserdisc/protocol/ListP.scala index 06ff67d9..6902886e 100644 --- a/core/src/main/scala/laserdisc/protocol/ListP.scala +++ b/core/src/main/scala/laserdisc/protocol/ListP.scala @@ -14,65 +14,54 @@ object ListP { } } -trait ListP { - import ListP.Position - import Read.==> +trait ListBaseP { import shapeless._ - private[this] final val minusOneIsNone = RESPRead.instance(Read.integerMinusOneIsNone[PosInt]) - private[this] final val zeroIsNone = RESPRead.instance(Read.integerZeroIsNone[PosInt]) + final object listtypes { + final type ListPosition = ListP.Position - final object lists { - final val position = Position + final val ListPosition = ListP.Position } - final def lindex[A](key: Key, index: Index)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Option[A]] = - Protocol("LINDEX", key :: index :: HNil).asC[NullBulkString :+: NonNullBulkString :+: CNil, Option[A]] + import listtypes._ - final def linsert[A: Show](key: Key, position: Position, pivot: A, value: A): Protocol.Aux[Option[PosInt]] = + private[this] final val minusOneIsNone = RESPRead.instance(Read.numMinusOneIsNone[PosInt]) + private[this] final val zeroIsNone = RESPRead.instance(Read.numZeroIsNone[PosInt]) + + final def lindex[A: Bulk ==> ?](key: Key, index: Index): Protocol.Aux[Option[A]] = + Protocol("LINDEX", key :: index :: HNil).opt[GenBulk].as[A] + + final def linsert[A: Show](key: Key, position: ListPosition, pivot: A, value: A): Protocol.Aux[Option[PosInt]] = Protocol("LINSERT", key :: position :: pivot :: value :: HNil).using(minusOneIsNone) - final def llen(key: Key): Protocol.Aux[NonNegInt] = Protocol("LLEN", key).as[Integer, NonNegInt] + final def llen(key: Key): Protocol.Aux[NonNegInt] = Protocol("LLEN", key).as[Num, NonNegInt] - final def lpop[A](key: Key)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Option[A]] = Protocol("LPOP", key).asC[NullBulkString :+: NonNullBulkString :+: CNil, Option[A]] + final def lpop[A: Bulk ==> ?](key: Key): Protocol.Aux[Option[A]] = Protocol("LPOP", key).opt[GenBulk].as[A] final def lpush[A: Show](key: Key, values: OneOrMore[A]): Protocol.Aux[PosInt] = - Protocol("LPUSH", key :: values.value :: HNil).as[Integer, PosInt] + Protocol("LPUSH", key :: values.value :: HNil).as[Num, PosInt] - final def lpushx[A: Show](key: Key, value: A): Protocol.Aux[Option[PosInt]] = - Protocol("LPUSHX", key :: value :: HNil).using(zeroIsNone) + final def lpushx[A: Show](key: Key, value: A): Protocol.Aux[Option[PosInt]] = Protocol("LPUSHX", key :: value :: HNil).using(zeroIsNone) - final def lrange[A](key: Key, start: Index, end: Index)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[A]] = Protocol("LRANGE", key :: start :: end :: HNil).as[NonNilArray, Seq[A]] + final def lrange[A: Bulk ==> ?](key: Key, start: Index, end: Index): Protocol.Aux[Seq[A]] = + Protocol("LRANGE", key :: start :: end :: HNil).as[Arr, Seq[A]] final def lrem[A: Show](key: Key, count: Index, value: A): Protocol.Aux[NonNegInt] = - Protocol("LREM", key :: count :: value :: HNil).as[Integer, NonNegInt] + Protocol("LREM", key :: count :: value :: HNil).as[Num, NonNegInt] - final def lset[A: Show](key: Key, index: Index, value: A): Protocol.Aux[OK] = - Protocol("LSET", key :: index :: value :: HNil).as[SimpleString, OK] + final def lset[A: Show](key: Key, index: Index, value: A): Protocol.Aux[OK] = Protocol("LSET", key :: index :: value :: HNil).as[Str, OK] - final def ltrim(key: Key, start: Index, stop: Index): Protocol.Aux[OK] = - Protocol("LTRIM", key :: start :: stop :: HNil).as[SimpleString, OK] + final def ltrim(key: Key, start: Index, stop: Index): Protocol.Aux[OK] = Protocol("LTRIM", key :: start :: stop :: HNil).as[Str, OK] - final def rpop[A](key: Key)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Option[A]] = Protocol("RPOP", key).asC[NullBulkString :+: NonNullBulkString :+: CNil, Option[A]] + final def rpop[A: Bulk ==> ?](key: Key): Protocol.Aux[Option[A]] = Protocol("RPOP", key).opt[GenBulk].as[A] - final def rpoplpush[A](source: Key, destination: Key)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Option[A]] = - Protocol("RPOPLPUSH", source :: destination :: Nil).asC[NullBulkString :+: NonNullBulkString :+: CNil, Option[A]] + final def rpoplpush[A: Bulk ==> ?](source: Key, destination: Key): Protocol.Aux[Option[A]] = + Protocol("RPOPLPUSH", source :: destination :: Nil).opt[GenBulk].as[A] final def rpush[A: Show](key: Key, values: OneOrMore[A]): Protocol.Aux[PosInt] = - Protocol("RPUSH", key :: values.value :: HNil).as[Integer, PosInt] + Protocol("RPUSH", key :: values.value :: HNil).as[Num, PosInt] - final def rpushx[A: Show](key: Key, value: A): Protocol.Aux[Option[PosInt]] = - Protocol("RPUSHX", key :: value :: HNil).using(zeroIsNone) + final def rpushx[A: Show](key: Key, value: A): Protocol.Aux[Option[PosInt]] = Protocol("RPUSHX", key :: value :: HNil).using(zeroIsNone) } -trait AllListP extends ListP with ListPExtra +trait ListP extends ListBaseP with ListExtP diff --git a/core/src/main/scala/laserdisc/protocol/Protocol.scala b/core/src/main/scala/laserdisc/protocol/Protocol.scala index 11a8b594..1919c2b4 100644 --- a/core/src/main/scala/laserdisc/protocol/Protocol.scala +++ b/core/src/main/scala/laserdisc/protocol/Protocol.scala @@ -5,7 +5,7 @@ import shapeless._ sealed trait Request { def command: String - def parameters: Seq[BulkString] + def parameters: Seq[GenBulk] } sealed trait Response { type A } @@ -27,8 +27,8 @@ sealed trait Protocol extends Request with Response { self => override def encode(protocol: Protocol): RESP = self.codec.encode(protocol) override def decode(resp: RESP): Maybe[B] = self.codec.decode(resp).right.map(f(_)) } - override def command: String = self.command - override def parameters: Seq[BulkString] = self.parameters + override def command: String = self.command + override def parameters: Seq[GenBulk] = self.parameters } override final def toString: String = s"Protocol($command)" @@ -37,13 +37,15 @@ sealed trait Protocol extends Request with Response { self => object Protocol { final type Aux[A0] = Protocol { type A = A0 } - final class RESPProtocolCodec[A <: Coproduct, B](val R: RESPRead.Aux[A, B]) extends AnyVal with ProtocolCodec[B] { - import RESP._ - - override def encode(value: Protocol): RESP = arr(bulk(value.command), value.parameters: _*) + final class RESPProtocolCodec[A, B](val R: RESPRead.Aux[A, B]) extends AnyVal with ProtocolCodec[B] { + override def encode(value: Protocol): RESP = Arr(Bulk(value.command), value.parameters: _*) override def decode(resp: RESP): Maybe[B] = R.read(resp) } + final class PartiallyAppliedOptionalProtocol[L, A](private val underlying: PartiallyAppliedProtocol[L]) extends AnyVal { + def as[B](implicit R: RESPRead.Aux[A, Option[B]]): Protocol.Aux[Option[B]] = underlying.asC(R) + } + sealed abstract class PartiallyAppliedProtocol[L: RESPParamWrite]( private[this] final val command: String, private[this] final val l: L @@ -63,15 +65,17 @@ object Protocol { * @return A fully-fledged [[Protocol]] for the provided [[Request]]/[[Response]] * pair */ - final def asC[A <: Coproduct, B](implicit R: RESPRead.Aux[A, B]): Protocol.Aux[B] = new Protocol { + final def asC[A, B](implicit R: RESPRead.Aux[A, B]): Protocol.Aux[B] = new Protocol { override final type A = B - override final val codec: ProtocolCodec[B] = new RESPProtocolCodec(R) - override final val command: String = self.command - override final val parameters: Seq[BulkString] = RESPParamWrite[L].write(l) + override final val codec: ProtocolCodec[B] = new RESPProtocolCodec(R) + override final val command: String = self.command + override final val parameters: Seq[GenBulk] = RESPParamWrite[L].write(l) } final def as[A, B](implicit ev: A <:!< Coproduct, R: RESPRead.Aux[A :+: CNil, B]): Protocol.Aux[B] = asC(R) + final def opt[A](implicit ev: A <:!< Coproduct, gen: Generic[A]) = new PartiallyAppliedOptionalProtocol[L, gen.Repr](self) + final def using[A <: Coproduct, B](R: RESPRead.Aux[A, B]): Protocol.Aux[B] = asC(R) } @@ -79,10 +83,9 @@ object Protocol { * * This apply method requires the caller to provide the type of request parameters L this * [[Protocol]] expects to deal with when encoding the request parameters into a [[RESP]] - * [[Array]] instance to send to Redis. + * [[GenArr]] instance to send to Redis. * * */ - final def apply[L: RESPParamWrite](cmd: String, l: L): PartiallyAppliedProtocol[L] = - new PartiallyAppliedProtocol(cmd, l) {} + final def apply[L: RESPParamWrite](cmd: String, l: L): PartiallyAppliedProtocol[L] = new PartiallyAppliedProtocol(cmd, l) {} } diff --git a/core/src/main/scala/laserdisc/protocol/PublishP.scala b/core/src/main/scala/laserdisc/protocol/PublishP.scala index e158f58a..df3991f8 100644 --- a/core/src/main/scala/laserdisc/protocol/PublishP.scala +++ b/core/src/main/scala/laserdisc/protocol/PublishP.scala @@ -7,5 +7,5 @@ trait PublishP { //TODO work out all pub/sub system properly final def publish[A: Show](channel: Key, value: A): Protocol.Aux[NonNegInt] = - Protocol("PUBLISH", channel :: value :: HNil).as[Integer, NonNegInt] + Protocol("PUBLISH", channel :: value :: HNil).as[Num, NonNegInt] } diff --git a/core/src/main/scala/laserdisc/protocol/RESP.scala b/core/src/main/scala/laserdisc/protocol/RESP.scala index fe4b7eac..387a62dc 100644 --- a/core/src/main/scala/laserdisc/protocol/RESP.scala +++ b/core/src/main/scala/laserdisc/protocol/RESP.scala @@ -6,12 +6,13 @@ import java.{lang => j} import scodec.Decoder.decodeCollect import scodec.Encoder.encodeSeq -import scodec.Err.MatchingDiscriminatorNotFound +import scodec.Err.{General, MatchingDiscriminatorNotFound} import scodec.bits.{BitVector, _} import scodec.codecs.{filtered, fixedSizeBytes} -import scodec.{Attempt, Codec, DecodeResult, Decoder, Err, SizeBound} +import scodec.{Attempt, Codec, DecodeResult, Decoder, SizeBound, Err => SErr} import scala.annotation.tailrec +import shapeless.Generic /** [[https://redis.io/topics/protocol Redis Protocol Specification]] * @@ -23,97 +24,55 @@ import scala.annotation.tailrec * @see [[RESPBuilders]] * @see [[RESPCodecs]] */ -sealed trait RESP extends Any with Serializable +sealed trait RESP extends AnyRef with Serializable /** * RESP [[https://redis.io/topics/protocol#resp-simple-strings Simple Strings]] * - * These can be constructed by using the [[RESPBuilders#str]] method - * * @note Sometimes the value "OK" is used to represent a successful * acknowledgement/processing of a command. * * @example * {{{ - * import laserdisc.protocol.RESP._ - * - * val simpleString: SimpleString = str("some string") + * val s: Str = Str("some string") * }}} * * @param value The wrapped string value */ -final class SimpleString private[protocol] (val value: String) extends RESP { - override def hashCode(): Int = value.hashCode - override def equals(obj: Any): Boolean = obj match { - case other: SimpleString => other.value == value - case _ => false - } - override def toString: String = s"SimpleString($value)" -} -object SimpleString { - final def unapply(simpleString: SimpleString): Option[String] = Some(simpleString.value) +final case class Str(value: String) extends RESP +object Str { + final def apply[A](a: A)(implicit A: Show[A]): Str = new Str(A.show(a)) } /** * RESP [[https://redis.io/topics/protocol#resp-errors Errors]] * - * RESP [[Error]]s are also [[scala.RuntimeException]]s, although + * RESP [[Err]]s are also [[scala.RuntimeException]]s, although * __where possible__ they will not contain stacktrace data * - * These can be constructed by using the [[RESPBuilders#err]] method - * * @example * {{{ - * import laserdisc.protocol.RESP._ - * - * val error: Error = err("some error message") + * val e: Err = Err("some error message") * }}} - * * @param message The wrapped exception's message */ -final class Error private[protocol] (val message: String) - extends laserdisc.Platform.LaserDiscRuntimeError(message) - with RESP { - override def hashCode(): Int = message.hashCode - override def equals(obj: Any): Boolean = obj match { - case other: Error => other.message == message - case _ => false - } - override def toString: String = s"Error($message)" -} -object Error { - final def unapply(error: Error): Option[String] = Some(error.message) -} +final case class Err(message: String) extends laserdisc.Platform.LaserDiscRuntimeError(message) with RESP /** * RESP [[https://redis.io/topics/protocol#resp-integers Integers]] * - * These can be constructed by using the [[RESPBuilders#int]] method - * * @note Sometimes the values 0 and 1 are used to represent boolean * values. In this case 0 corresponds to False while 1 to True, * respectively. * * @example * {{{ - * import laserdisc.protocol.RESP._ - * - * val integer: Integer = int(42) + * val n: Num = Num(42) * }}} * * @param value The wrapped long value */ -final class Integer private[protocol] (val value: Long) extends RESP { - override def hashCode(): Int = value.hashCode() - override def equals(obj: Any): Boolean = obj match { - case other: Integer => other.value == value - case _ => false - } - override def toString: String = s"Integer($value)" -} -object Integer { - final def unapply(integer: Integer): Option[Long] = Some(integer.value) -} +final case class Num(value: Long) extends RESP /** * RESP [[https://redis.io/topics/protocol#resp-bulk-strings Bulk Strings]] @@ -122,46 +81,29 @@ object Integer { * - `null` bulk strings, where the length is -1 and no actual underlying string is present * - actual (non-null) bulk strings, where the length is >= 0 * - * Non-null [[BulkString]]s can be constructed using the [[RESPBuilders#bulk]] - * method - * - * @note A forwarder for `null` [[BulkString]]s is present too and represented - * using the `final val`s [[RESPBuilders.nullBulk]] - * * @example * {{{ - * import laserdisc.protocol.RESP._ - * - * val nonNullBulkString: NonNullBulkString = bulk("some string") - * val nullBulkString: NullBulkString = nullBulk + * val b: Bulk = Bulk("some string") + * val nb: NullBulk = NullBulk * }}} - * * @see [[Show]] */ -sealed trait BulkString extends RESP -sealed trait NullBulkString extends BulkString -case object NullBulkString extends NullBulkString +sealed trait GenBulk extends RESP +case object NullBulk extends GenBulk /** - * This is the special case of a non-null RESP [[BulkString]] + * This is the special case of a non-null RESP [[GenBulk]] * * These can be constructed by using the [[RESPBuilders#bulk]] * method * * @param value The wrapped bulk string value */ -final class NonNullBulkString private[protocol] (val value: String) extends BulkString { - override def hashCode(): Int = value.hashCode - override def equals(obj: Any): Boolean = obj match { - case other: NonNullBulkString => other.value == value - case _ => false - } - override def toString: String = s"BulkString($value)" -} -object NonNullBulkString { - final def unapply(nonNullBulkString: NonNullBulkString): Option[String] = Some(nonNullBulkString.value) +final case class Bulk(value: String) extends GenBulk +object Bulk { + final def apply[A](a: A)(implicit A: Show[A]): Bulk = new Bulk(A.show(a)) - implicit final val nonNullBulkStringShow: Show[NonNullBulkString] = Show.instance(_.value) + implicit final val bulkShow: Show[Bulk] = Show.instance(_.value) } /** @@ -171,378 +113,253 @@ object NonNullBulkString { * - `nil` arrays, where the length is -1 and no array element is present * - actual (non-nil) arrays, where the length is >= 0 * - * Non-nil [[Array]]s can be constructed using the - * [[[RESPBuilders#arr(xs:Seq[laserdisc\.protocol\.RESP])* RESPBuilders#arr(xs: Seq[RESP])]]] method. - * - * @note [[[RESPBuilders#arr(one:laserdisc\.protocol\.RESP,rest:laserdisc\.protocol\.RESP*)* RESPBuilders#arr(one: RESP, rest: RESP*)]]] + * @note [[[Arr#apply(one:laserdisc\.protocol\.RESP,rest:laserdisc\.protocol\.RESP*)* Arr#apply(one: RESP, rest: RESP*)]]] * is an overload which supports the creation of guaranteed non-empty * sequences only. This is achieved through the usage of one fixed * parameter followed by a var-arg of the same - * - * @note A forwarder for `nil` is present too and represented using - * the `final val`s [[RESPBuilders.nilArray]] - * * @example * {{{ - * import laserdisc.protocol.RESP._ - * - * val nonNilArray: NonNilArray = arr(Vector(str("hello"), str("world"))) - * val guaranteedNonEmptyArray: NonNilArray = arr(str("hello"), str("world")) - * val emptyArray: NonNilArray = arr(Vector.empty) - * val nilArray: NilArray = nilArray + * val arr: Arr = Arr(List(Str("hello"), Str("world"))) + * val guaranteedNonEmpty: Arr = Arr(Str("hello"), Str("world")) + * val empty: Arr = Arr(List.empty) + * val nil: NilArr = NilArr * }}} */ -sealed trait Array extends RESP -sealed trait NilArray extends Array -case object NilArray extends NilArray +sealed trait GenArr extends RESP +case object NilArr extends GenArr /** - * This is the special case of a non-nil RESP [[Array]] - * - * These can be constructed by using the [[[RESPBuilders#arr(xs:Seq[laserdisc\.protocol\.RESP])* RESPBuilders#arr(xs: Seq[RESP])]]] - * method + * This is the special case of a non-nil RESP [[GenArr]] * - * __or__ - * - * by resorting to the overloaded - * [[[RESPBuilders#arr(one:laserdisc\.protocol\.RESP,rest:laserdisc\.protocol\.RESP*)* RESPBuilders#arr(one: RESP, rest: RESP*)]]] + * These can be constructed either by the default case class' apply by resorting to the overloaded + * [[[Arr#apply(one:laserdisc\.protocol\.RESP,rest:laserdisc\.protocol\.RESP*)* Arr#apply(one: RESP, rest: RESP*)]]] * method which expects one parameter to be supplied followed * by a (possibly empty) sequence of [[RESP]]s (vararg). * - * @param elements The wrapped array values, as a [[scala.Vector]] of [[RESP]] + * @param elements The wrapped array values, as a [[scala.List]] of [[RESP]] */ -final class NonNilArray private[protocol] (val elements: Vector[RESP]) extends Array { - override def hashCode(): Int = elements.hashCode() - override def equals(obj: Any): Boolean = obj match { - case other: NonNilArray => other.elements == elements - case _ => false - } - override def toString: String = s"Array(${elements.mkString(",")})" -} -object NonNilArray { - final def unapply(nonEmptyArray: NonNilArray): Option[Vector[RESP]] = Some(nonEmptyArray.elements) +final case class Arr(elements: List[RESP]) extends GenArr { + override def toString: String = s"Arr(${elements.mkString(COMMA)})" } - -private[protocol] final case class Representation[A](decoded: A, bits: BitVector) - -sealed trait RESPBuilders { - final def str(value: String): SimpleString = new SimpleString(value) - - final def err(message: String): Error = new Error(message) - - final def int(value: Long): Integer = new Integer(value) - - final val nullBulk: NullBulkString = NullBulkString - final def bulk(s: String): NonNullBulkString = new NonNullBulkString(s) - - final val nilArray: NilArray = NilArray - final def arr(one: RESP, rest: RESP*): NonNilArray = arr(one +: rest) - final def arr(xs: Seq[RESP]): NonNilArray = new NonNilArray(xs.toVector) +object Arr { + final def apply(one: RESP, rest: RESP*): Arr = new Arr(one +: rest.toList) } -sealed trait RESPCodecs extends BitVectorSyntax { this: RESPBuilders => - - protected final val utf8 = new LenientStringCodec(UTF_8) - protected final val BitsInByte = 8L - protected final val plus :: minus :: colon :: dollar :: star :: crlf :: minusOne :: zero :: Nil = - (hex"2b" :: hex"2d" :: hex"3a" :: hex"24" :: hex"2a" :: hex"0d0a" :: hex"2d31" :: hex"30" :: Nil) - .map(_.bits) +private[protocol] final case class Repr[A](decoded: A, bits: BitVector) + +sealed trait RESPCodecs extends BitVectorSyntax { + protected final val utf8Codec = new LenientStringCodec(UTF_8) + protected final val BitsInByte = 8L + protected final val plus = hex"2b".bits + protected final val minus = hex"2d".bits + protected final val colon = hex"3a".bits + protected final val dollar = hex"24".bits + protected final val star = hex"2a".bits + protected final val crlf = hex"0d0a".bits + protected final val minusOne = hex"2d31".bits + protected final val zero = hex"30".bits private[this] final val crlfSize = crlf.size - protected final val crlfBytes = crlf.bytes + protected final val crlfBytes = crlf.bytes private[this] final val crlfBytesSize = crlfBytes.size - private[this] final def crlfTerminatedStartingAtPosition[A](startingAt: Long)(codecForA: Codec[A]): Codec[A] = - filtered( - codecForA, - new Codec[BitVector] { - override final def sizeBound: SizeBound = SizeBound.unknown - override final def encode(bits: BitVector): Attempt[BitVector] = Attempt.successful(bits ++ crlf) - override final def decode(bits: BitVector): Attempt[DecodeResult[BitVector]] = - bits.bytes.indexOfSlice(crlfBytes, startingAt) match { - case -1 => Attempt.failure(new MatchingDiscriminatorNotFound(s"Does not contain 'CRLF' termination bytes. Content: ${bits.tailToUtf8()}")) - case i => Attempt.successful(DecodeResult(bits.take(i * BitsInByte), bits.drop(i * BitsInByte + crlfSize))) - } + private[this] final def crlfTerminatedCodec[A](baseCodec: Codec[A], from: Long = 0L): Codec[A] = filtered( + baseCodec, + new Codec[BitVector] { + override final def sizeBound: SizeBound = SizeBound.unknown + override final def encode(bits: BitVector): Attempt[BitVector] = Attempt.successful(bits ++ crlf) + override final def decode(bits: BitVector): Attempt[DecodeResult[BitVector]] = bits.bytes.indexOfSlice(crlfBytes, from) match { + case -1 => + Attempt.failure(new MatchingDiscriminatorNotFound(s"Does not contain 'CRLF' termination bytes. Content: ${bits.tailToUtf8}")) + case i => Attempt.successful(DecodeResult(bits.take(i * BitsInByte), bits.drop(i * BitsInByte + crlfSize))) } - ).withToString("crlf-terminated string") - - protected final val representationOfString: Codec[Representation[String]] = - new Codec[Representation[String]] { - final override def sizeBound: SizeBound = - SizeBound.unknown - - final override def encode(bd: Representation[String]): Attempt[BitVector] = - utf8.encode(bd.decoded) map (_ ++ crlf) - - final override def decode(bits: BitVector): Attempt[DecodeResult[Representation[String]]] = - utf8.decode(bits) map { res => res map (Representation(_, bits)) } } - - private[this] final val firstCrlfTerminatedString: Codec[String] = - crlfTerminatedStartingAtPosition(0L)(utf8) - - private[this] final val firstCrlfTerminatedReprOfString: Codec[Representation[String]] = - crlfTerminatedStartingAtPosition(0L)(representationOfString) - - protected final val crlfTerminatedReprOfLong: Codec[Representation[Long]] = - firstCrlfTerminatedReprOfString.narrow[Representation[Long]]( - bds => - try Attempt.successful(Representation(j.Long.parseLong(bds.decoded), bds.bits)) - catch { case _: NumberFormatException => Attempt.failure(Err(s"Expected long but found ${bds.decoded}")) }, - bdl => Representation(bdl.decoded.toString, bdl.bits) - ) - .withToString("crlf-terminated string repr of long and the decoded bits") - - protected final val longAsCRLFTerminatedString: Codec[Long] = - firstCrlfTerminatedString.narrow[Long]( - s => - try Attempt.successful(j.Long.parseLong(s)) - catch { case _: NumberFormatException => Attempt.failure(Err(s"Expected long but found $s")) }, - _.toString - ) - .withToString("crlf-terminated string repr of long") - - private[this] final val simpleStringCodec: Codec[SimpleString] = - firstCrlfTerminatedString.xmap[SimpleString](str, _.value).withToString("simple-string") - - private[this] final val errorCodec: Codec[Error] = - firstCrlfTerminatedString.xmap[Error](err, _.message).withToString("error") - - private[this] final val integerCodec: Codec[Integer] = - longAsCRLFTerminatedString.xmap[Integer](int, _.value).withToString("integer") - - private[this] final val bulkStringCodec: Codec[BulkString] = new Codec[BulkString] { - - private[this] final val nullBulkStringBits = minusOne ++ crlf - - private[this] final val decoder = longAsCRLFTerminatedString.flatMap { - case -1 => Decoder.point(NullBulkString) - case size if size >= 0 => fixedSizeBytes(size + crlfBytesSize, crlfTerminatedStartingAtPosition(size)(utf8)).map(bulk) + ) + + private[this] final val crlfTerminatedStringCodec: Codec[String] = crlfTerminatedCodec(utf8Codec) + private[this] final val crlfTerminatedLongCodec: Codec[Long] = crlfTerminatedStringCodec.narrow( + s => + try Attempt.successful(j.Long.parseLong(s)) + catch { case _: NumberFormatException => Attempt.failure(SErr(s"Expected long but found $s")) }, + _.toString + ) + private[this] final val strCodec: Codec[Str] = crlfTerminatedStringCodec.xmap[Str](Str.apply, _.value) + private[this] final val errCodec: Codec[Err] = crlfTerminatedStringCodec.xmap[Err](Err.apply, _.message) + private[this] final val numCodec: Codec[Num] = crlfTerminatedLongCodec.xmap[Num](Num.apply, _.value) + private[this] final val bulkCodec: Codec[GenBulk] = new Codec[GenBulk] { + private[this] final val nullBulkBits = minusOne ++ crlf + private[this] final val decoder = crlfTerminatedLongCodec.flatMap { + case -1 => Decoder.point(NullBulk) + case size if size >= 0 => fixedSizeBytes(size + crlfBytesSize, crlfTerminatedCodec(utf8Codec, size)).map(Bulk.apply) case negSize => Decoder.liftAttempt(Attempt.failure(failDec(negSize))) } - - private[this] final def failDec(negSize: Long) = - Err.General(s"failed to decode bulk-string of size $negSize", List("size")) - - private[this] final def failEnc(bulkString: BulkString, err: Err) = - Err.General(s"failed to encode size of [$bulkString]: ${err.messageWithContext}", List("size")) + private[this] final def failDec(negSize: Long) = General(s"failed to decode bulk-string of size $negSize", List("size")) + private[this] final def failEnc(bulk: GenBulk, err: SErr) = + General(s"failed to encode size of [$bulk]: ${err.messageWithContext}", List("size")) override final def sizeBound: SizeBound = SizeBound.unknown - - override final def encode(bulkString: BulkString): Attempt[BitVector] = - bulkString match { - case NullBulkString => Attempt.successful(nullBulkStringBits) - case NonNullBulkString(s) => - firstCrlfTerminatedString.encode(s).flatMap { bits => - longAsCRLFTerminatedString - .encode(bits.size / BitsInByte - crlfBytesSize) - .mapErr(failEnc(bulkString, _)) - .map(_ ++ bits) - } - } - - override final def decode(buffer: BitVector): Attempt[DecodeResult[BulkString]] = decoder.decode(buffer) - - override final def toString: String = "bulk-string" + override final def encode(bulk: GenBulk): Attempt[BitVector] = bulk match { + case NullBulk => Attempt.successful(nullBulkBits) + case Bulk(s) => + crlfTerminatedStringCodec.encode(s).flatMap { bits => + crlfTerminatedLongCodec.encode(bits.size / BitsInByte - crlfBytesSize).mapErr(failEnc(bulk, _)).map(_ ++ bits) + } + } + override final def decode(buffer: BitVector): Attempt[DecodeResult[GenBulk]] = decoder.decode(buffer) } - - protected final val arrayCodec: Codec[Array] = new Codec[Array] { - - private[this] final val nilArrayBits = minusOne ++ crlf - - private[this] final val emptyArrayBits = zero ++ crlf - - private[this] final def checkSize(v: Vector[RESP], expectedSize: Long) = + private[this] final val arrCodec: Codec[GenArr] = new Codec[GenArr] { + private[this] final val nilArrBits = minusOne ++ crlf + private[this] final val emptyArrBits = zero ++ crlf + private[this] final def checkSize(v: List[RESP], expectedSize: Long): Attempt[List[RESP]] = if (v.size == expectedSize) Attempt.successful(v) - else Attempt.failure(Err(s"Insufficient number of elements: decoded ${v.size} instead of $expectedSize")) - - private[this] final val decoder = longAsCRLFTerminatedString.flatMap { - case -1 => Decoder.point(NilArray) - case 0 => Decoder.point(arr(Seq.empty)) + else Attempt.failure(SErr(s"Insufficient number of elements: decoded ${v.size} instead of $expectedSize")) + private[this] final val decoder = crlfTerminatedLongCodec.flatMap { + case -1 => Decoder.point(NilArr) + case 0 => Decoder.point(Arr(List.empty)) case size if size > 0 => - Decoder(decodeCollect[Vector, RESP](respCodec, Some(size.toInt))(_)) - .narrow[Vector[RESP]](checkSize(_, size), identity) - .map(arr(_)) + Decoder(decodeCollect[List, RESP](respCodec, Some(size.toInt))(_)).narrow[List[RESP]](checkSize(_, size), identity).map(Arr.apply) case negSize => Decoder.liftAttempt(Attempt.failure(failDec(negSize))) } - - private[this] final def failDec(negSize: Long) = - Err.General(s"failed to decode array of size $negSize", List("size")) - - private[this] final def failEnc(array: Array, err: Err) = - Err.General(s"failed to encode size of [$array]: ${err.messageWithContext}", List("size")) + private[this] final def failDec(negSize: Long) = General(s"failed to decode array of size $negSize", List("size")) + private[this] final def failEnc(arr: GenArr, err: SErr) = + General(s"failed to encode size of [$arr]: ${err.messageWithContext}", List("size")) override final def sizeBound: SizeBound = SizeBound.unknown - - override final def encode(array: Array): Attempt[BitVector] = array match { - case NilArray => Attempt.successful(nilArrayBits) - case NonNilArray(v) if v.isEmpty => Attempt.successful(emptyArrayBits) - case NonNilArray(v) => - longAsCRLFTerminatedString - .encode(v.size.toLong) - .mapErr(failEnc(array, _)) - .flatMap(size => encodeSeq(respCodec)(v).map(size ++ _)) + override final def encode(arr: GenArr): Attempt[BitVector] = arr match { + case NilArr => Attempt.successful(nilArrBits) + case Arr(v) if v.isEmpty => Attempt.successful(emptyArrBits) + case Arr(v) => + crlfTerminatedLongCodec.encode(v.size.toLong).mapErr(failEnc(arr, _)).flatMap(size => encodeSeq(respCodec)(v).map(size ++ _)) } - override final def decode(bits: BitVector): Attempt[DecodeResult[Array]] = decoder.decode(bits) - override final def toString: String = "array" + override final def decode(bits: BitVector): Attempt[DecodeResult[GenArr]] = decoder.decode(bits) } implicit final val respCodec: Codec[RESP] = new Codec[RESP] { - override final def sizeBound: SizeBound = SizeBound.unknown - override final def encode(value: RESP): Attempt[BitVector] = value match { - case simpleString : SimpleString => simpleStringCodec.encode(simpleString).map(plus ++ _) - case error : Error => errorCodec.encode(error).map(minus ++ _) - case integer : Integer => integerCodec.encode(integer).map(colon ++ _) - case bulkString : BulkString => bulkStringCodec.encode(bulkString).map(dollar ++ _) - case array : Array => arrayCodec.encode(array).map(star ++ _) + case str: Str => strCodec.encode(str).map(plus ++ _) + case err: Err => errCodec.encode(err).map(minus ++ _) + case num: Num => numCodec.encode(num).map(colon ++ _) + case bulk: GenBulk => bulkCodec.encode(bulk).map(dollar ++ _) + case arr: GenArr => arrCodec.encode(arr).map(star ++ _) } - override final def decode(bits: BitVector): Attempt[DecodeResult[RESP]] = bits.consumeThen(BitsInByte)( - error => Attempt.failure(Err(error)), - { - case (taken, remainder) => (taken match { - case `plus` => Attempt.successful(simpleStringCodec) - case `minus` => Attempt.successful(errorCodec) - case `colon` => Attempt.successful(integerCodec) - case `dollar` => Attempt.successful(bulkStringCodec) - case `star` => Attempt.successful(arrayCodec) - case other => Attempt.failure(Err(s"unidentified RESP type (Hex: ${other.toHex})")) - }) flatMap (_.decode(remainder)) + s => Attempt.failure(SErr(s)), { + case (`plus`, remainder) => strCodec.decode(remainder) + case (`minus`, remainder) => errCodec.decode(remainder) + case (`colon`, remainder) => numCodec.decode(remainder) + case (`dollar`, remainder) => bulkCodec.decode(remainder) + case (`star`, remainder) => arrCodec.decode(remainder) + case (other, _) => Attempt.failure(SErr(s"unidentified RESP type (Hex: ${other.toHex})")) } ) - override final def toString: String = "RESP" } + + protected final val crlfTerminatedReprOfLongDecoder: Decoder[Repr[Long]] = crlfTerminatedCodec( + new Codec[Repr[String]] { + override final def sizeBound: SizeBound = SizeBound.unknown + override final def encode(bd: Repr[String]): Attempt[BitVector] = utf8Codec.encode(bd.decoded) + override final def decode(bits: BitVector): Attempt[DecodeResult[Repr[String]]] = utf8Codec.decode(bits).map(_.map(Repr(_, bits))) + } + ).asDecoder.emap { sRepr => + try Attempt.successful(Repr(j.Long.parseLong(sRepr.decoded), sRepr.bits)) + catch { case _: NumberFormatException => Attempt.failure(SErr(s"Expected long but found ${sRepr.decoded}")) } + } +} + +sealed trait RESPCoproduct { + final val gen = Generic[RESP] + + final type RESPCoproduct = gen.Repr } -sealed trait RESPFunctions { this: RESPCodecs => +sealed trait RESPFunctions extends EitherSyntax { this: RESPCodecs => import BitVectorDecoding._ - final val stateOf: BitVector => String | BitVectorState = - bits => bits.consumeThen(BitsInByte)( - error => Left(error), - { - case (typeToken, payload) => (typeToken match { - case `plus` - | `minus` - | `colon` => Right(NoSize) - case `dollar` => Right(BulkSize) - case `star` => Right(CollectionSize) - case other => Left(s"unidentified RESP type when checking the state: unexpected value ${other.toUtf8} (Hex: ${other.toHex})") - }) flatMap { - case BulkSize => - evalWithSizeDecodedFrom(payload) { - case Left(_) => Incomplete - case Right(DecodeResult(value, remainder)) => - val decodedSize = BitsInByte + value.bits.size + crlf.size - val expectedBulkSize = (value.decoded * BitsInByte) + crlf.size - val completeBulkSize = decodedSize + expectedBulkSize - - if (value.decoded >= 0 && remainder.size == expectedBulkSize) - Complete - - else if (value.decoded >= 0 && remainder.size > expectedBulkSize) - CompleteWithRemainder(bits.take(completeBulkSize), bits.drop(completeBulkSize)) - - else if (value.decoded == -1 && remainder.isEmpty) - Complete - - else if (value.decoded == -1 && remainder.nonEmpty) - CompleteWithRemainder(bits.take(decodedSize), bits.drop(decodedSize)) - - else MissingBits(expectedBulkSize - remainder.size) - } - - case CollectionSize => - failingEvalWithSizeDecodedFrom(payload) { - case Left(_) => - Right(Incomplete) - - case Right(DecodeResult(value, remainder)) => - val decodedSize = BitsInByte + value.bits.size + crlf.size - - if (value.decoded == -1 && remainder.isEmpty) - Right(Complete) - - else if (value.decoded == -1 && remainder.nonEmpty) - Right(CompleteWithRemainder(bits.take(decodedSize), bits.drop(decodedSize))) - - else stateOfArray(value.decoded, remainder, bits.take(decodedSize)) - } - - case NoSize => - val endOfMessageByte = bits.bytes.indexOfSlice(crlfBytes, 0L) - val completeMessageSize = endOfMessageByte * BitsInByte + crlf.size - - if (endOfMessageByte == -1) - Right(Incomplete) - else if (completeMessageSize < bits.size) - Right( CompleteWithRemainder( - bits.take(completeMessageSize), - bits.drop(completeMessageSize) - )) - else - Right(Complete) + private[this] final val readDiscriminator: BitVector => ((BitVector, BitVector) => String | State) => String | State = + bits => f => bits.consumeThen(BitsInByte)(_.asLeft, f) + + final val stateOf: BitVector => String | State = bits => + readDiscriminator(bits) { + case (`plus` | `minus` | `colon`, _) => + val eomIndex = bits.bytes.indexOfSlice(crlfBytes, from = 0L) + val size = eomIndex * BitsInByte + crlf.size + + if (eomIndex == -1) Right(Incomplete) + else if (size < bits.size) Right(CompleteWithRemainder(bits.take(size), bits.drop(size))) + else Right(Complete) + case (`dollar`, payload) => + evalWithSizeDecodedFrom(payload) { + case Left(_) => Incomplete + case Right(DecodeResult(value, remainder)) => + val decoded = BitsInByte + value.bits.size + crlf.size + val expected = value.decoded * BitsInByte + crlf.size + val size = decoded + expected + + if (value.decoded >= 0 && remainder.size == expected) Complete + else if (value.decoded >= 0 && remainder.size > expected) CompleteWithRemainder(bits.take(size), bits.drop(size)) + else if (value.decoded == -1 && remainder.isEmpty) Complete + else if (value.decoded == -1 && remainder.nonEmpty) CompleteWithRemainder(bits.take(decoded), bits.drop(decoded)) + else MissingBits(expected - remainder.size) } - } - ) + case (`star`, payload) => + attemptEvalWithSizeDecodedFrom(payload) { + case Left(_) => Right(Incomplete) + case Right(DecodeResult(value, remainder)) => + val size = BitsInByte + value.bits.size + crlf.size + + if (value.decoded == -1 && remainder.isEmpty) Right(Complete) + else if (value.decoded == -1 && remainder.nonEmpty) Right(CompleteWithRemainder(bits.take(size), bits.drop(size))) + else stateOfArr(value.decoded, remainder, bits.take(size)) + } + case (other, _) => Left(s"unidentified RESP type when checking the state: ${other.toUtf8} (${other.toHex})") + } @tailrec - private[this] final def stateOfArray(stillMissing: Long, remainder: BitVector, soFar: BitVector): String | BitVectorState = - stillMissing match { - case 0L => - if (remainder.isEmpty) Right(Complete) - else Right(CompleteWithRemainder(soFar, remainder)) - - case _ => - stateOf(remainder) match { - case Left(e) => Left(e) - case Right(state) => state match { - - case CompleteWithRemainder(c, r) => - stateOfArray(stillMissing - 1, r, soFar ++ c) - - case Complete => - if (stillMissing == 1) Right(Complete) - else Right(Incomplete) - - case incomplete @ (MissingBits(_) | Incomplete) => - Right(incomplete) - } - } - } + private[this] final def stateOfArr(missing: Long, remainder: BitVector, bits: BitVector): String | State = missing match { + case 0L => + if (remainder.isEmpty) Right(Complete) + else Right(CompleteWithRemainder(bits, remainder)) + + case _ => + stateOf(remainder) match { + case Left(e) => Left(e) + case Right(CompleteWithRemainder(c, r)) => stateOfArr(missing - 1, r, bits ++ c) + case Right(Complete) if missing == 1 => Right(Complete) + case Right(Complete) => Right(Incomplete) + case Right(incomplete) => Right(incomplete) + } + } + + private[this] final def evalWithSizeDecodedFrom[A](bits: BitVector)(f: (Incomplete | DecodeResult[Repr[Long]]) => A): String | A = + attemptEvalWithSizeDecodedFrom(bits)(x => Right(f(x))) - private[this] final def evalWithSizeDecodedFrom[A](bits: BitVector)(f: (Incomplete | DecodeResult[Representation[Long]]) => A): String | A = - failingEvalWithSizeDecodedFrom(bits)(x => Right(f(x))) - - private[this] final def failingEvalWithSizeDecodedFrom[A](bits: BitVector)(f: (Incomplete | DecodeResult[Representation[Long]]) => String | A): String | A = - crlfTerminatedReprOfLong.decode(bits).fold ( - { - case MatchingDiscriminatorNotFound(_, _) => f(Left(Incomplete)) - case error => Left(error.message) - }, - res => f(Right(res)) - ) - - private[this] sealed trait SizeType - private[this] final case object BulkSize extends SizeType - private[this] final case object CollectionSize extends SizeType - private[this] final case object NoSize extends SizeType + private[this] final def attemptEvalWithSizeDecodedFrom[A](bits: BitVector)( + f: (Incomplete | DecodeResult[Repr[Long]]) => String | A + ): String | A = + crlfTerminatedReprOfLongDecoder + .decode(bits) + .fold( + { + case MatchingDiscriminatorNotFound(_, _) => f(Left(Incomplete)) + case err => Left(err.message) + }, + res => f(Right(res)) + ) } object BitVectorDecoding { type Incomplete = Incomplete.type - type Complete = Complete.type + type Complete = Complete.type - sealed trait BitVectorState extends Product with Serializable - final case class MissingBits(stillToReceive: Long) extends BitVectorState - final case class CompleteWithRemainder(complete: BitVector, remainder: BitVector) extends BitVectorState - final case object Incomplete extends BitVectorState - final case object Complete extends BitVectorState + sealed trait State extends Product with Serializable + final case class MissingBits(stillToReceive: Long) extends State + final case class CompleteWithRemainder(complete: BitVector, remainder: BitVector) extends State + final case object Incomplete extends State + final case object Complete extends State } -object RESP extends RESPBuilders with RESPCodecs with RESPFunctions +object RESP extends RESPCodecs with RESPCoproduct with RESPFunctions diff --git a/core/src/main/scala/laserdisc/protocol/RESPFrame.scala b/core/src/main/scala/laserdisc/protocol/RESPFrame.scala index 864aca5e..7f267e90 100644 --- a/core/src/main/scala/laserdisc/protocol/RESPFrame.scala +++ b/core/src/main/scala/laserdisc/protocol/RESPFrame.scala @@ -4,63 +4,49 @@ package protocol import java.nio.ByteBuffer import laserdisc.protocol.BitVectorDecoding._ +import laserdisc.protocol.RESP.stateOf import scodec.bits.BitVector import scala.annotation.tailrec sealed trait RESPFrame extends Product with Serializable with EitherSyntax with BitVectorSyntax { - - def append(bytes: ByteBuffer): Exception | NonEmptyRESPFrame = - nextFrame(BitVector.view(bytes)) - + def append(bytes: ByteBuffer): Exception | NonEmptyRESPFrame = nextFrame(BitVector.view(bytes)) protected final def nextFrame(bits: BitVector): Exception | NonEmptyRESPFrame = - RESP.stateOf(bits) flatMap { - case MissingBits(n) => IncompleteFrame(bits, n).asRight - case Incomplete => IncompleteFrame(bits, 0L).asRight - case Complete => CompleteFrame(bits).asRight - case CompleteWithRemainder(c, r) => consumeRemainder(MoreThanOneFrame(CompleteFrame(c) :: Nil, r).asRight) - } leftMap (e => new Exception(s"Error building the frame: $e. Content: ${bits.tailToUtf8()}")) - - @tailrec - private[this] final def consumeRemainder(current: String | MoreThanOneFrame): String | MoreThanOneFrame = - current match { - case Left(e) => e.asLeft - case Right(s) => RESP.stateOf(s.remainder) match { - case Left(ee) => ee.asLeft - case Right(rs) => rs match { - - case CompleteWithRemainder(c, r) => - consumeRemainder( - MoreThanOneFrame(CompleteFrame(c) :: s.invertedComplete, r).asRight - ) - - case Complete => - MoreThanOneFrame(CompleteFrame(s.remainder) :: s.invertedComplete, BitVector.empty).asRight - - case MissingBits(_) | Incomplete => - s.asRight - } + stateOf(bits) + .flatMap { + case MissingBits(n) => IncompleteFrame(bits, n).asRight + case Incomplete => IncompleteFrame(bits, 0L).asRight + case Complete => CompleteFrame(bits).asRight + case CompleteWithRemainder(c, r) => consumeRemainder(MoreThanOneFrame(CompleteFrame(c) :: Nil, r).asRight) } - } + .leftMap(e => new Exception(s"Err building the frame: $e. Content: ${bits.tailToUtf8}")) + + @tailrec private[this] final def consumeRemainder(current: String | MoreThanOneFrame): String | MoreThanOneFrame = current match { + case Left(e) => e.asLeft + case Right(s) => + stateOf(s.remainder) match { + case Left(ee) => ee.asLeft + case Right(CompleteWithRemainder(c, r)) => consumeRemainder(MoreThanOneFrame(CompleteFrame(c) :: s.invertedComplete, r).asRight) + case Right(Complete) => MoreThanOneFrame(CompleteFrame(s.remainder) :: s.invertedComplete, BitVector.empty).asRight + case _ => s.asRight + } + } } -private[protocol] sealed trait NonEmptyRESPFrame extends Product with Serializable +case object EmptyFrame extends RESPFrame +private[protocol] sealed trait NonEmptyRESPFrame extends RESPFrame -case object EmptyFrame extends RESPFrame +final case class CompleteFrame(bits: BitVector) extends NonEmptyRESPFrame +final case class MoreThanOneFrame(private[protocol] val invertedComplete: List[CompleteFrame], remainder: BitVector) + extends NonEmptyRESPFrame { -final case class CompleteFrame(bits: BitVector) extends RESPFrame with NonEmptyRESPFrame -final case class MoreThanOneFrame(private[protocol] val invertedComplete: List[CompleteFrame], remainder: BitVector) extends RESPFrame with NonEmptyRESPFrame { - def complete: Vector[CompleteFrame] = - invertedComplete.foldRight(Vector.empty[CompleteFrame])((c, v) => v :+ c) + def complete: Vector[CompleteFrame] = invertedComplete.foldRight(Vector.empty[CompleteFrame])((c, v) => v :+ c) } -final case class IncompleteFrame(partial: BitVector, bitsToComplete: Long) extends RESPFrame with NonEmptyRESPFrame { - +final case class IncompleteFrame(partial: BitVector, bitsToComplete: Long) extends NonEmptyRESPFrame { override def append(bytes: ByteBuffer): Exception | NonEmptyRESPFrame = { - val newBits = BitVector.view(bytes) - // Saves some size checks if (bitsToComplete > 0 && bitsToComplete == newBits.size) CompleteFrame(partial ++ newBits).asRight else nextFrame(partial ++ newBits) } -} \ No newline at end of file +} diff --git a/core/src/main/scala/laserdisc/protocol/RESPParamWrite.scala b/core/src/main/scala/laserdisc/protocol/RESPParamWrite.scala index 741b9c07..6cd2b690 100644 --- a/core/src/main/scala/laserdisc/protocol/RESPParamWrite.scala +++ b/core/src/main/scala/laserdisc/protocol/RESPParamWrite.scala @@ -6,29 +6,32 @@ import shapeless.labelled.FieldType import scala.annotation.implicitNotFound @implicitNotFound( - "Implicit not found: RESPParamWrite[${A}].\n\n" + - "Normally you would not need to define one manually, as one will be derived for you automatically iff:\n" + - "- an instance of Show[${A}] is in scope\n" + - "- ${A} is a List whose LUB has a RESPParamWrite instance defined\n" + - "- ${A} is an HList whose elements all have a RESPParamWrite instance defined\n" + """Implicit not found RESPParamWrite[${A}]. + +Normally you would not need to define one manually, as one will be derived for you automatically iff: +- an instance of Show[${A}] is in scope +- ${A} is a List whose LUB has a RESPParamWrite instance defined +- ${A} is an HList whose elements all have a RESPParamWrite instance defined +""" ) trait RESPParamWrite[A] { - def write(a: A): Seq[BulkString] + + def write(a: A): Seq[GenBulk] } -object RESPParamWrite extends LowPriorityRESPParamWrite { +object RESPParamWrite extends RESPParamWriteInstances { @inline final def apply[A](implicit instance: RESPParamWrite[A]): RESPParamWrite[A] = instance - final def const[A](thunk: => Seq[BulkString]): RESPParamWrite[A] = new RESPParamWrite[A] { - override def write(a: A): Seq[BulkString] = thunk + final def const[A](thunk: => Seq[GenBulk]): RESPParamWrite[A] = new RESPParamWrite[A] { + override def write(a: A): Seq[GenBulk] = thunk } - final def instance[A](f: A => Seq[BulkString]): RESPParamWrite[A] = new RESPParamWrite[A] { - override def write(a: A): Seq[BulkString] = f(a) + final def instance[A](f: A => Seq[GenBulk]): RESPParamWrite[A] = new RESPParamWrite[A] { + override def write(a: A): Seq[GenBulk] = f(a) } } -trait LowPriorityRESPParamWrite extends LowestPriorityRESPParamWrite { - implicit final def showRESPParamWrite[A](implicit A: Show[A]): RESPParamWrite[A] = RESPParamWrite.instance { a => - Seq(RESP.bulk(A.show(a))) +private[protocol] sealed trait RESPParamWriteInstances extends RESPParamWriteInstances1 { + implicit final def showRESPParamWrite[A: Show]: RESPParamWrite[A] = RESPParamWrite.instance { a => + Seq(Bulk(a)) } implicit final def pairRESPParamWrite[A, B]( implicit A: RESPParamWrite[A], @@ -41,12 +44,10 @@ trait LowPriorityRESPParamWrite extends LowestPriorityRESPParamWrite { implicit final def taggedRESPParamWrite[K <: Symbol, V]( implicit K: Witness.Aux[K], V: RESPParamWrite[V] - ): RESPParamWrite[FieldType[K, V]] = RESPParamWrite.instance { v => - RESP.bulk(K.value.name) +: V.write(v) - } + ): RESPParamWrite[FieldType[K, V]] = RESPParamWrite.instance(Bulk(K.value.name) +: V.write(_)) } -sealed trait LowestPriorityRESPParamWrite { +private[protocol] sealed trait RESPParamWriteInstances1 { implicit final val nilRESPParamWrite: RESPParamWrite[Nil.type] = RESPParamWrite.const(Seq.empty) implicit final val hNilRESPParamWrite: RESPParamWrite[HNil] = RESPParamWrite.const(Seq.empty) diff --git a/core/src/main/scala/laserdisc/protocol/RESPRead.scala b/core/src/main/scala/laserdisc/protocol/RESPRead.scala index cf2a738b..efcfc550 100644 --- a/core/src/main/scala/laserdisc/protocol/RESPRead.scala +++ b/core/src/main/scala/laserdisc/protocol/RESPRead.scala @@ -7,61 +7,59 @@ import shapeless.ops.coproduct._ import scala.annotation.implicitNotFound @implicitNotFound( - "Implicit not found: RESPRead[${A}].\n\n" + - "You should not need to define one manually, as one will be derived for you automatically iff:\n" + - "- evidence of a Read instance from some sum/co-product to ${A} can be provided\n" + - "- this sum/co-product is a subset of the sum-co-product for RESP" + """Implicit not found RESPRead[${A}]. + +You should not need to define one manually, as one will be derived for you automatically iff: +- evidence of a Read instance from some sum/co-product to ${A} can be provided +- this sum/co-product is a subset of the sum-co-product for RESP + """ ) trait RESPRead[A] { + type Sub + def read(resp: RESP): Maybe[A] } object RESPRead { - import Read.==> import RESP._ final type Aux[Sub0, A] = RESPRead[A] { type Sub = Sub0 } final def apply[Sub, A](implicit instance: RESPRead.Aux[Sub, A]): RESPRead.Aux[Sub, A] = instance - private[this] final type RESPCoproduct = - SimpleString :+: - Error :+: - Integer :+: - NullBulkString :+: NonNullBulkString :+: - NilArray :+: NonNilArray :+: CNil - private[this] implicit val respInject: Inject[RESPCoproduct, RESP] = new Inject[RESPCoproduct, RESP] { override def apply(resp: RESP): RESPCoproduct = resp match { - case simpleString: SimpleString => Inl(simpleString) - case error: Error => Inr(Inl(error)) - case integer: Integer => Inr(Inr(Inl(integer))) - case NullBulkString => Inr(Inr(Inr(Inl(nullBulk)))) - case nonNullBulkString: NonNullBulkString => Inr(Inr(Inr(Inr(Inl(nonNullBulkString))))) - case NilArray => Inr(Inr(Inr(Inr(Inr(Inl(nilArray)))))) - case nonEmptyArray: NonNilArray => Inr(Inr(Inr(Inr(Inr(Inr(Inl(nonEmptyArray))))))) + case arr: Arr => Inl(arr) + case bulk: Bulk => Inr(Inl(bulk)) + case err: Err => Inr(Inr(Inl(err))) + case NilArr => Inr(Inr(Inr(Inl(NilArr)))) + case NullBulk => Inr(Inr(Inr(Inr(Inl(NullBulk))))) + case num: Num => Inr(Inr(Inr(Inr(Inr(Inl(num)))))) + case str: Str => Inr(Inr(Inr(Inr(Inr(Inr(Inl(str))))))) } } sealed abstract class DefaultRESPRead[A <: Coproduct, B, Rest <: Coproduct](R: A ==> B)( implicit ev0: Basis.Aux[RESPCoproduct, A, Rest], - ev1: Selector[Rest, Error] + ev1: Selector[Rest, Err] ) extends RESPRead[B] { override final type Sub = A override def read(resp: RESP): Maybe[B] = Coproduct[RESPCoproduct](resp).deembed match { case Right(R(b)) => Right(b) - case Right(_) => Left(err(s"RESP type(s) matched but failed to deserialize correctly: $resp")) - case Left(rest) => rest.select[Error].fold(Left(err(s"RESP type(s) did not match: $resp")))(Left(_)) + case Right(_) => + Left(Err(s"RESP type(s) matched but failed to deserialize correctly: $resp")).widenLeft[Throwable] + case Left(rest) => + rest.select[Err].fold(Left(Err(s"RESP type(s) did not match: $resp")))(Left(_)).widenLeft[Throwable] } } final def instance[A <: Coproduct, B, Rest <: Coproduct](R: A ==> B)( implicit ev0: Basis.Aux[RESPCoproduct, A, Rest], - ev1: Selector[Rest, Error] + ev1: Selector[Rest, Err] ): RESPRead.Aux[A, B] = new DefaultRESPRead(R) {} implicit final def derive[A <: Coproduct, B, Rest <: Coproduct]( implicit R: A ==> B, basis: Basis.Aux[RESPCoproduct, A, Rest], - selector: Selector[Rest, Error] + selector: Selector[Rest, Err] ): RESPRead.Aux[A, B] = new DefaultRESPRead(R) {} } diff --git a/core/src/main/scala/laserdisc/protocol/Read.scala b/core/src/main/scala/laserdisc/protocol/Read.scala index 1cb905dc..738ff7a2 100644 --- a/core/src/main/scala/laserdisc/protocol/Read.scala +++ b/core/src/main/scala/laserdisc/protocol/Read.scala @@ -7,154 +7,130 @@ import shapeless.labelled._ import scala.annotation.implicitNotFound @implicitNotFound( - "Implicit not found: Read[${A}, ${B}].\n\n" + - "Try writing your own, for example:\n\n" + - "implicit final val myRead: Read[${A}, ${B}] = new Read[${A}, ${B}] {\n" + - " override final def read(a: ${A}): Option[${B}] = ???\n" + - "}\n\n" + - "Note 1: you can use the factory method Read.instance[PF] instead of creating it manually as shown above\n" + - "Note 2: make sure to inspect the combinators as you may be able to leverage some other Read instance\n" + """Implicit not found Read[${A}, ${B}]. + +Try writing your own, for example: + +implicit final val myRead: Read[${A}, ${B}] = new Read[${A}, ${B}] { + override final def read(a: ${A}): Option[${B}] = ??? +} + +Note 1: you can use the factory methods Read.instance / Read.instancePF instead of creating it manually as shown above +Note 2: make sure to inspect the combinators as you may be able to leverage some other Read instance +""" ) trait Read[A, B] { self => def read(a: A): Option[B] - final def map[C](f: B => C): Read[A, C] = new Read[A, C] { - override final def read(a: A): Option[C] = self.read(a).map(f) - } + final def map[C](f: B => C): Read[A, C] = Read.instance(read(_).map(f)) - final def flatMap[C](f: B => Read[A, C]): Read[A, C] = new Read[A, C] { - override final def read(a: A): Option[C] = self.read(a).flatMap(b => f(b).read(a)) - } + final def flatMap[C](f: B => Read[A, C]): Read[A, C] = Read.instance(a => read(a).flatMap(f(_).read(a))) - final def contramap[C](f: C => A): Read[C, B] = new Read[C, B] { - override final def read(c: C): Option[B] = self.read(f(c)) - } + final def contramap[C](f: C => A): Read[C, B] = Read.instance(read _ compose f) - final def zip[C, D](RC: Read[C, D]): Read[(A, C), (B, D)] = new Read[(A, C), (B, D)] { - override def read(ac: (A, C)): Option[(B, D)] = { - val (a, c) = ac - for { - b <- self.read(a) - d <- RC.read(c) - } yield b -> d - } + final def orElse[C](other: Read[A, C]): Read[A, B | C] = Read.instancePF { + case `self`(b) => Left(b) + case `other`(c) => Right(c) } final def unapply(a: A): Option[B] = read(a) } -object Read extends LowPriorityReadInstances { - @inline final def apply[A, B](implicit instance: A ==> B): A ==> B = instance +object Read extends ReadInstances0 { + @inline final def apply[A, B](implicit instance: Read[A, B]): Read[A, B] = instance - final def instance[A, B](f: A => Option[B]): A ==> B = new Read[A, B] { + final def instance[A, B](f: A => Option[B]): Read[A, B] = new Read[A, B] { override def read(a: A): Option[B] = f(a) } - final def instancePF[A, B](pf: PartialFunction[A, B]): A ==> B = instance(pf.lift) + final def instancePF[A, B](pf: PartialFunction[A, B]): Read[A, B] = instance(pf.lift) final def lift2OptionWhen[A, B](cond: A => Boolean)( - implicit R: A ==> B - ): (A :+: CNil) ==> Option[B] = Read.instancePF { + implicit R: Read[A, B] + ): Read[A :+: CNil, Option[B]] = Read.instancePF { case Inl(i @ R(b)) if !cond(i) => Some(b) case Inl(_) => None } - final def integerMinusOneIsNone[A]( - implicit ev: Integer ==> A - ): (Integer :+: CNil) ==> Option[A] = lift2OptionWhen(_.value == -1L) - final def integerZeroIsNone[A]( - implicit ev: Integer ==> A - ): (Integer :+: CNil) ==> Option[A] = lift2OptionWhen(_.value == 0L) + final def numMinusOneIsNone[A: Read[Num, ?]]: Read[Num :+: CNil, Option[A]] = + lift2OptionWhen(_.value == -1L) - implicit final def identity[A]: A ==> A = instance(Some(_)) + final def numZeroIsNone[A: Read[Num, ?]]: Read[Num :+: CNil, Option[A]] = + lift2OptionWhen(_.value == 0L) } -trait LowPriorityReadInstances extends LowerPriorityReadInstances { - implicit final val simpleString2StringRead: SimpleString ==> String = Read.instance { - case SimpleString(s) => Some(s) - } - implicit final val simpleString2OKRead: SimpleString ==> OK = Read.instancePF { - case SimpleString("OK") => OK - } - implicit final val simpleString2KeyRead: SimpleString ==> Key = Read.instancePF { - case SimpleString(Key(s)) => s - } +trait ReadInstances0 extends ReadInstances1 { + implicit final def identity[A]: Read[A, A] = Read.instance(Some(_)) +} - implicit final val integer2BooleanRead: Integer ==> Boolean = Read.instancePF { - case Integer(0L) => false - case Integer(1L) => true - } - implicit final val integer2IntRead: Integer ==> Int = Read.instancePF { //TODO: maybe useless - case Integer(ToInt(i)) => i - } - implicit final val integer2LongRead: Integer ==> Long = Read.instancePF { - case Integer(l) => l - } - implicit final val integer2NonNegIntRead: Integer ==> NonNegInt = Read.instancePF { - case Integer(ToInt(NonNegInt(i))) => i - } - implicit final val integer2NonNegLongRead: Integer ==> NonNegLong = Read.instancePF { - case Integer(NonNegLong(l)) => l - } - implicit final val integer2NonZeroIntRead: Integer ==> NonZeroInt = Read.instancePF { - case Integer(ToInt(NonZeroInt(i))) => i - } - implicit final val integer2NonZeroLongRead: Integer ==> NonZeroLong = Read.instancePF { - case Integer(NonZeroLong(l)) => l - } - implicit final val integer2PosIntRead: Integer ==> PosInt = Read.instancePF { - case Integer(ToInt(PosInt(i))) => i - } - implicit final val integer2PosLongRead: Integer ==> PosLong = Read.instancePF { - case Integer(PosLong(l)) => l +trait ReadInstances1 extends ReadInstances2 { + implicit final val str2StringRead: Read[Str, String] = Read.instance(s => Some(s.value)) + implicit final val str2OKRead: Read[Str, OK] = Read.instancePF { case Str("OK") => OK } + implicit final val str2KeyRead: Read[Str, Key] = Read.instancePF { case Str(Key(s)) => s } + + implicit final val num2BooleanRead: Read[Num, Boolean] = Read.instancePF { + case Num(0L) => false; case Num(1L) => true + } + implicit final val num2IntRead: Read[Num, Int] = Read.instancePF { case Num(ToInt(i)) => i } + implicit final val num2LongRead: Read[Num, Long] = Read.instancePF { case Num(l) => l } + implicit final val num2NonNegIntRead: Read[Num, NonNegInt] = Read.instancePF { case Num(ToInt(NonNegInt(i))) => i } + implicit final val num2NonNegLongRead: Read[Num, NonNegLong] = Read.instancePF { case Num(NonNegLong(l)) => l } + implicit final val num2NonZeroIntRead: Read[Num, NonZeroInt] = Read.instancePF { case Num(ToInt(NonZeroInt(i))) => i } + implicit final val num2NonZeroLongRead: Read[Num, NonZeroLong] = Read.instancePF { case Num(NonZeroLong(l)) => l } + implicit final val num2PosIntRead: Read[Num, PosInt] = Read.instancePF { case Num(ToInt(PosInt(i))) => i } + implicit final val num2PosLongRead: Read[Num, PosLong] = Read.instancePF { case Num(PosLong(l)) => l } + implicit final val num2SlotRead: Read[Num, Slot] = Read.instancePF { case Num(ToInt(Slot(i))) => i } + + implicit final val bulk2StringRead: Read[Bulk, String] = Read.instance { case Bulk(s) => Some(s) } + implicit final val bulk2DoubleRead: Read[Bulk, Double] = Read.instancePF { case Bulk(ToDouble(d)) => d } + implicit final val bulk2IntRead: Read[Bulk, Int] = Read.instancePF { case Bulk(ToInt(i)) => i } + implicit final val bulk2LongRead: Read[Bulk, Long] = Read.instancePF { case Bulk(ToLong(l)) => l } + implicit final val bulk2ValidDoubleRead: Read[Bulk, ValidDouble] = Read.instancePF { case Bulk(ToDouble(ValidDouble(d))) => d } + implicit final val bulk2NonNegIntRead: Read[Bulk, NonNegInt] = Read.instancePF { case Bulk(ToInt(NonNegInt(i))) => i } + implicit final val bulk2NonNegLongRead: Read[Bulk, NonNegLong] = Read.instancePF { case Bulk(ToLong(NonNegLong(l))) => l } + implicit final val bulk2NonNegDoubleRead: Read[Bulk, NonNegDouble] = Read.instancePF { case Bulk(ToDouble(NonNegDouble(d))) => d } + implicit final val bulk2NonZeroDoubleRead: Read[Bulk, NonZeroDouble] = Read.instancePF { case Bulk(ToDouble(NonZeroDouble(d))) => d } + implicit final val bulk2NonZeroIntRead: Read[Bulk, NonZeroInt] = Read.instancePF { case Bulk(ToInt(NonZeroInt(i))) => i } + implicit final val bulk2NonZeroLongRead: Read[Bulk, NonZeroLong] = Read.instancePF { case Bulk(ToLong(NonZeroLong(l))) => l } + implicit final val bulk2PosIntRead: Read[Bulk, PosInt] = Read.instancePF { case Bulk(ToInt(PosInt(i))) => i } + implicit final val bulk2PosLongRead: Read[Bulk, PosLong] = Read.instancePF { case Bulk(ToLong(PosLong(l))) => l } + implicit final val bulk2ConnectionNameRead: Read[Bulk, ConnectionName] = Read.instancePF { case Bulk(ConnectionName(cn)) => cn } + implicit final val bulk2KeyRead: Read[Bulk, Key] = Read.instancePF { case Bulk(Key(k)) => k } + implicit final val bulk2GeoHashRead: Read[Bulk, GeoHash] = Read.instancePF { case Bulk(GeoHash(gh)) => gh } + + implicit final def arrOfBulk2Seq[A](implicit R: Read[Bulk, A]): Read[Arr, Seq[A]] = Read.instance { + case Arr(vector) => + val (vectorLength, (as, asLength)) = vector.foldLeft(0 -> (List.empty[A] -> 0)) { + case ((vl, (as0, asl)), R(a)) => (vl + 1) -> ((a :: as0) -> (asl + 1)) + case ((vl, acc), _) => (vl + 1) -> acc + } + if (vectorLength == asLength) Some(as.reverse) else None + case _ => None } - implicit final val nonNullBulkString2StringRead: NonNullBulkString ==> String = Read.instance { - case NonNullBulkString(s) => Some(s) - } - implicit final val nonNullBulkString2DoubleRead: NonNullBulkString ==> Double = Read.instancePF { - case NonNullBulkString(ToDouble(d)) => d - } - implicit final val nonNullBulkString2IntRead: NonNullBulkString ==> Int = Read.instancePF { //TODO: maybe useless - case NonNullBulkString(ToInt(i)) => i - } - implicit final val nonNullBulkString2LongRead: NonNullBulkString ==> Long = Read.instancePF { - case NonNullBulkString(ToLong(l)) => l - } - implicit final val nonNullBulkString2ValidDoubleRead: NonNullBulkString ==> ValidDouble = Read.instancePF { - case NonNullBulkString(ToDouble(ValidDouble(d))) => d - } - implicit final val nonNullBulkString2NonNegIntRead: NonNullBulkString ==> NonNegInt = Read.instancePF { - case NonNullBulkString(ToInt(NonNegInt(i))) => i - } - implicit final val nonNullBulkString2NonNegLongRead: NonNullBulkString ==> NonNegLong = Read.instancePF { - case NonNullBulkString(ToLong(NonNegLong(l))) => l - } - implicit final val nonNullBulkString2NonZeroDoubleRead: NonNullBulkString ==> NonZeroDouble = Read.instancePF { - case NonNullBulkString(ToDouble(NonZeroDouble(d))) => d - } - implicit final val nonNullBulkString2NonZeroIntRead: NonNullBulkString ==> NonZeroInt = Read.instancePF { - case NonNullBulkString(ToInt(NonZeroInt(i))) => i - } - implicit final val nonNullBulkString2NonZeroLongRead: NonNullBulkString ==> NonZeroLong = Read.instancePF { - case NonNullBulkString(ToLong(NonZeroLong(l))) => l - } - implicit final val nonNullBulkString2PosIntRead: NonNullBulkString ==> PosInt = Read.instancePF { - case NonNullBulkString(ToInt(PosInt(i))) => i - } - implicit final val nonNullBulkString2PosLongRead: NonNullBulkString ==> PosLong = Read.instancePF { - case NonNullBulkString(ToLong(PosLong(l))) => l - } - implicit final val nonNullBulkString2ConnectionNameRead: NonNullBulkString ==> ConnectionName = Read.instancePF { - case NonNullBulkString(ConnectionName(connectionName)) => connectionName + implicit final def arrOfBulk2OptionSeq[A](implicit R: Read[Bulk, A]): Read[Arr, Seq[Option[A]]] = Read.instance { + case Arr(vector) => + val (vectorLength, (as, asLength)) = vector.foldLeft(0 -> (List.empty[Option[A]] -> 0)) { + case ((vl, (as0, asl)), NullBulk) => (vl + 1) -> ((None :: as0) -> (asl + 1)) + case ((vl, (as0, asl)), R(a)) => (vl + 1) -> ((Some(a) :: as0) -> (asl + 1)) + case ((vl, acc), _) => (vl + 1) -> acc + } + if (vectorLength == asLength) Some(as.reverse) else None + case _ => None } - implicit final val nonNullBulkString2KeyRead: NonNullBulkString ==> Key = Read.instancePF { - case NonNullBulkString(Key(s)) => s + + implicit final def arrOfArr2OptionSeq[A](implicit R: Read[Arr, A]): Read[Arr, Seq[Option[A]]] = Read.instance { + case Arr(vector) => + val (vectorLength, (as, asLength)) = vector.foldLeft(0 -> (List.empty[Option[A]] -> 0)) { + case ((vl, (as0, asl)), NilArr) => (vl + 1) -> ((None :: as0) -> (asl + 1)) + case ((vl, (as0, asl)), R(a)) => (vl + 1) -> ((Some(a) :: as0) -> (asl + 1)) + case ((vl, acc), _) => (vl + 1) -> acc + } + if (vectorLength == asLength) Some(as.reverse) else None + case _ => None } - implicit final def nonNilArray2Seq[A]( - implicit R: NonNullBulkString ==> A - ): NonNilArray ==> Seq[A] = Read.instance { - case NonNilArray(vector) => + implicit final def arrOfArr2Seq[A](implicit R: Read[Arr, A]): Read[Arr, Seq[A]] = Read.instance { + case Arr(vector) => val (vectorLength, (as, asLength)) = vector.foldLeft(0 -> (List.empty[A] -> 0)) { case ((vl, (as0, asl)), R(a)) => (vl + 1) -> ((a :: as0) -> (asl + 1)) case ((vl, acc), _) => (vl + 1) -> acc @@ -163,119 +139,96 @@ trait LowPriorityReadInstances extends LowerPriorityReadInstances { case _ => None } - implicit final def nonNilArray2Tuple2Read[A, B]( - implicit RA: NonNullBulkString ==> A, - RB: NonNullBulkString ==> B - ): NonNilArray ==> (A, B) = Read.instancePF { - case NonNilArray(RA(key) +: RB(value) +: Seq()) => key -> value - } - - implicit final def nonNilArray2Tuple2Seq[A, B]( - implicit RA: NonNullBulkString ==> A, - RB: NonNullBulkString ==> B - ): NonNilArray ==> Seq[(A, B)] = Read.instance { - case NonNilArray(vector) => - val (vectorLength, (kvs, kvsLength)) = + implicit final def arr2Tuple2Read[A, B](implicit RA: Read[Bulk, A], RB: Read[Bulk, B]): Read[Arr, (A, B)] = Read.instancePF { + case Arr(RA(a) +: RB(b) +: Seq()) => a -> b + } +// +// implicit final def arr2Tuple3Read[A, B, C]( +// implicit RA: Read[Bulk, A], +// RB: Read[Bulk, B] +// ): Read[Arr, (A, B)] = Read.instancePF { +// case Arr(RA(key) +: RB(value) +: Seq()) => key -> value +// } +// +// implicit final def arr2Tuple4Read[A, B, C, D]( +// implicit RA: Read[Bulk, A], +// RB: Read[Bulk, B] +// ): Read[Arr, (A, B)] = Read.instancePF { +// case Arr(RA(key) +: RB(value) +: Seq()) => key -> value +// } + + implicit final def arr2Tuple2Seq[A, B](implicit RA: Read[Bulk, A], RB: Read[Bulk, B]): Read[Arr, Seq[(A, B)]] = Read.instance { + case Arr(vector) => + val (vectorLength, (abs, absLength)) = vector.grouped(2).foldLeft(0 -> (List.empty[(A, B)] -> 0)) { - case ((vl, (kv, kvl)), RA(a) +: RB(b) +: Seq()) => (vl + 1) -> (((a -> b) :: kv) -> (kvl + 1)) - case ((vl, acc), _) => (vl + 1) -> acc + case ((vl, (abs0, absl)), RA(a) +: RB(b) +: Seq()) => (vl + 1) -> (((a -> b) :: abs0) -> (absl + 1)) + case ((vl, acc), _) => (vl + 1) -> acc } - if (vectorLength == kvsLength) Some(kvs.reverse) else None + if (vectorLength == absLength) Some(abs.reverse) else None case _ => None } - implicit final def nonNilArray2Scan[A]( - implicit R: NonNilArray ==> Seq[A] - ): NonNilArray ==> Scan[A] = Read.instance { - case NonNilArray(NonNullBulkString(ToLong(NonNegLong(cursor))) +: NilArray +: Seq()) => - Some(Scan(cursor, None)) - case NonNilArray(NonNullBulkString(ToLong(NonNegLong(cursor))) +: R(as) +: Seq()) => - Some(Scan(cursor, Some(as))) - case _ => None + implicit final def arr2KV[A](implicit R: Read[Bulk, A]): Read[Arr, KV[A]] = Read.instancePF { + case Arr(Bulk(Key(k)) +: R(a) +: Seq()) => KV(k, a) } - implicit final val nonNilArray2Map: NonNilArray ==> Map[Key, String] = Read.instance { - case NonNilArray(vector) => - val (vectorLength, (kvs, kvsLength)) = - vector.grouped(2).foldLeft(0 -> (Map.empty[Key, String] -> 0)) { - case ((vl, (kv, kvl)), NonNullBulkString(Key(k)) +: NonNullBulkString(v) +: Seq()) => - (vl + 1) -> ((kv + (k -> v)) -> (kvl + 1)) - case ((vl, acc), _) => (vl + 1) -> acc - } + implicit final def arr2Scan[A](implicit R: Read[Arr, Seq[A]]): Read[Arr, Scan[A]] = Read.instancePF { + case Arr(Bulk(ToLong(NonNegLong(cursor))) +: NilArr +: Seq()) => Scan(cursor, None) + case Arr(Bulk(ToLong(NonNegLong(cursor))) +: R(as) +: Seq()) => Scan(cursor, Some(as)) + } + + implicit final val arr2Map: Read[Arr, Map[Key, String]] = Read.instance { + case Arr(vector) => + val (vectorLength, (kvs, kvsLength)) = vector.grouped(2).foldLeft(0 -> (Map.empty[Key, String] -> 0)) { + case ((vl, (kv, kvl)), Bulk(Key(k)) +: Bulk(v) +: Seq()) => (vl + 1) -> ((kv + (k -> v)) -> (kvl + 1)) + case ((vl, acc), _) => (vl + 1) -> acc + } if (vectorLength == kvsLength) Some(kvs) else None case _ => None } - implicit final val nonNilArray2ScanKV: NonNilArray ==> ScanKV = Read.instance { - case NonNilArray(NonNullBulkString(ToLong(NonNegLong(cursor))) +: NilArray +: Seq()) => - Some(ScanKV(cursor, None)) - case NonNilArray(NonNullBulkString(ToLong(NonNegLong(cursor))) +: NonNilArray(vector) +: Seq()) => - val (vectorLength, (kvs, kvsLength)) = - vector.grouped(2).foldLeft(0 -> (List.empty[KV[String]] -> 0)) { - case ((vl, (kv, kvl)), NonNullBulkString(Key(k)) +: NonNullBulkString(v) +: Seq()) => - (vl + 1) -> ((KV(k, v) :: kv) -> (kvl + 1)) - case ((vl, acc), _) => (vl + 1) -> acc - } + implicit final val arr2ScanKV: Read[Arr, ScanKV] = Read.instance { + case Arr(Bulk(ToLong(NonNegLong(cursor))) +: NilArr +: Seq()) => Some(ScanKV(cursor, None)) + case Arr(Bulk(ToLong(NonNegLong(cursor))) +: Arr(vector) +: Seq()) => + val (vectorLength, (kvs, kvsLength)) = vector.grouped(2).foldLeft(0 -> (List.empty[KV[String]] -> 0)) { + case ((vl, (kv, kvl)), Bulk(Key(k)) +: Bulk(v) +: Seq()) => (vl + 1) -> ((KV(k, v) :: kv) -> (kvl + 1)) + case ((vl, acc), _) => (vl + 1) -> acc + } if (vectorLength == kvsLength) Some(ScanKV(cursor, Some(kvs.reverse))) else None case _ => None } - implicit final val nonNilArray2TimeRead: NonNilArray ==> Time = Read.instancePF { - case NonNilArray( - NonNullBulkString(ToLong(NonNegLong(ts))) +: NonNullBulkString(ToLong(NonNegLong(em))) +: Seq() - ) => - Time(ts, em) - } - - implicit final def nonNilArrayToProduct[P <: Product, L <: HList]( - implicit G: LabelledGeneric.Aux[P, L], - R: NonNilArray ==> L - ): NonNilArray ==> P = Read.instancePF { - case R(l) => G.from(l) + implicit final val arr2TimeRead: Read[Arr, Time] = Read.instancePF { + case Arr(Bulk(ToLong(NonNegLong(ts))) +: Bulk(ToLong(NonNegLong(em))) +: Seq()) => Time(ts, em) } - implicit final def nonNilArray2LabelledHCons[HK <: Symbol, HV, T <: HList]( + implicit final def arr2LabelledHCons[HK <: Symbol, HV, T <: HList]( implicit HK: Witness.Aux[HK], - RHV: NonNullBulkString ==> HV, - RT: NonNilArray ==> T - ): NonNilArray ==> (FieldType[HK, HV] :: T) = Read.instance { - case NonNilArray(NonNullBulkString(HK.value.`name`) +: RHV(hv) +: rest) => - RT.read(RESP.arr(rest)).map(t => field[HK](hv) :: t) - case _ => None + RHV: Read[Bulk, HV], + RT: Read[Arr, T] + ): Read[Arr, FieldType[HK, HV] :: T] = Read.instance { + case Arr(Bulk(HK.value.`name`) +: RHV(hv) +: rest) => RT.read(Arr(rest)).map(t => field[HK](hv) :: t) + case _ => None } - implicit final def nonNilArray2HCons[H, T <: HList]( - implicit - ev: H <:!< FieldType[_, _], - RH: NonNullBulkString ==> H, - RT: NonNilArray ==> T - ): NonNilArray ==> (H :: T) = Read.instance { - case NonNilArray(RH(h) +: rest) => RT.read(RESP.arr(rest)).map(h :: _) + implicit final def arr2HCons[H: <:!<[?, FieldType[_, _]], T <: HList]( + implicit RH: Read[Bulk, H], + RT: Read[Arr, T] + ): Read[Arr, H :: T] = Read.instance { + case Arr(RH(h) +: rest) => RT.read(Arr(rest)).map(h :: _) } - implicit final val nonNilArray2HNil: NonNilArray ==> HNil = Read.instancePF { - case NonNilArray(Seq()) => HNil - } + implicit final val arr2HNil: Read[Arr, HNil] = Read.instancePF { case Arr(Seq()) => HNil } } -sealed trait LowerPriorityReadInstances { - final type ==>[A, B] = Read[A, B] - - implicit final def liftNonNullBulkString2Option[A, B]( - implicit R: A ==> B - ): (NullBulkString :+: A :+: CNil) ==> Option[B] = Read.instancePF { - case Inl(_) => None - case Inr(Inl(R(b))) => Some(b) +sealed trait ReadInstances2 { + implicit final def liftNullBulk2Option[A, B](implicit R: Read[A, B]): Read[A :+: NullBulk :+: CNil, Option[B]] = Read.instancePF { + case Inl(R(b)) => Some(b) + case Inr(_) => None } - implicit final def liftNonNilArray2Option[A, B]( - implicit R: A ==> B - ): (NilArray :+: A :+: CNil) ==> Option[B] = Read.instancePF { - case Inl(_) => None - case Inr(Inl(R(b))) => Some(b) + implicit final def liftNilArr2Option[A, B](implicit R: Read[A, B]): Read[A :+: NilArr :+: CNil, Option[B]] = Read.instancePF { + case Inl(R(b)) => Some(b) + case Inr(_) => None } - - implicit final def liftSimpleToSum[A, B]( - implicit R: A ==> B, - ev: A <:!< Coproduct - ): (A :+: CNil) ==> B = Read.instancePF { + implicit final def liftSimpleToSum[A: <:!<[?, Coproduct], B](implicit R: Read[A, B]): Read[A :+: CNil, B] = Read.instancePF { case Inl(R(b)) => b } } diff --git a/core/src/main/scala/laserdisc/protocol/ServerP.scala b/core/src/main/scala/laserdisc/protocol/ServerP.scala index 3cd1a982..c03dfe0f 100644 --- a/core/src/main/scala/laserdisc/protocol/ServerP.scala +++ b/core/src/main/scala/laserdisc/protocol/ServerP.scala @@ -2,12 +2,12 @@ package laserdisc package protocol object ServerP { - import Read.==> - import scala.language.dynamics sealed trait InfoSection final object InfoSection { + final type default = default.type + final object server extends InfoSection final object clients extends InfoSection final object memory extends InfoSection @@ -21,7 +21,7 @@ object ServerP { final object all extends InfoSection final object default extends InfoSection - implicit val defaultShow: Show[default.type] = Show.const("default") + implicit val defaultShow: Show[default] = Show.const("default") implicit val infoSectionShow: Show[InfoSection] = Show.instance { case `server` => "server" case `clients` => "clients" @@ -60,75 +60,65 @@ object ServerP { final case object connected extends ReplicaStatus final case class Master(currentReplicationOffset: NonNegLong, clients: Seq[Client]) extends Role - final case class Slave(masterHost: Host, - masterPort: Port, - masterViewReplicationState: ReplicaStatus, - masterReplicationOffset: NonNegLong) + final case class Slave(masterHost: Host, masterPort: Port, masterViewReplicaStatus: ReplicaStatus, masterReplicationOffset: NonNegLong) extends Role final case class Sentinel(masterNames: Seq[Key]) extends Role - implicit val roleRead: NonNilArray ==> Role = { - val ClientRead: NonNilArray ==> Client = Read.instancePF { - case NonNilArray( - NonNullBulkString(Host(host)) +: NonNullBulkString(ToInt(Port(port))) +: NonNullBulkString( - ToLong(NonNegLong(offset))) +: Seq()) => - Client(host, port, offset) + implicit val roleRead: Arr ==> Role = { + val CR: Arr ==> Client = Read.instancePF { + case Arr(Bulk(Host(host)) +: Bulk(ToInt(Port(port))) +: Bulk(ToLong(NonNegLong(offset))) +: Seq()) => Client(host, port, offset) } - val ReplicaStatusRead: NonNullBulkString ==> ReplicaStatus = Read.instancePF { - case NonNullBulkString("connect") => connect - case NonNullBulkString("connecting") => connecting - case NonNullBulkString("sync") => sync - case NonNullBulkString("connected") => connected + val RSR: Bulk ==> ReplicaStatus = Read.instancePF { + case Bulk("connect") => connect + case Bulk("connecting") => connecting + case Bulk("sync") => sync + case Bulk("connected") => connected } - val MastersRead: NonNilArray ==> Seq[Key] = Read[NonNilArray, Seq[Key]] + val MR: Arr ==> Seq[Key] = Read[Arr, Seq[Key]] + Read.instance { - case NonNilArray(NonNullBulkString("master") +: Integer(NonNegLong(offset)) +: NonNilArray(v) +: Seq()) => + case Arr(Bulk("master") +: Num(NonNegLong(offset)) +: Arr(v) +: Seq()) => val (vLength, (clients, clientsLength)) = v.foldLeft(0 -> (List.empty[Client] -> 0)) { - case ((vl, (cs, csl)), ClientRead(client)) => (vl + 1) -> ((client :: cs) -> (csl + 1)) - case ((vl, acc), _) => (vl + 1) -> acc + case ((vl, (cs, csl)), CR(client)) => (vl + 1) -> ((client :: cs) -> (csl + 1)) + case ((vl, acc), _) => (vl + 1) -> acc } if (vLength == clientsLength) Some(Master(offset, clients.reverse)) else None - case NonNilArray( - NonNullBulkString("slave") +: NonNullBulkString(Host(host)) +: Integer(ToInt(Port(port))) +: ReplicaStatusRead( - replicaStatus) +: Integer(NonNegLong(offset)) +: Seq()) => + case Arr(Bulk("slave") +: Bulk(Host(host)) +: Num(ToInt(Port(port))) +: RSR(replicaStatus) +: Num(NonNegLong(offset)) +: Seq()) => Some(Slave(host, port, replicaStatus, offset)) - case NonNilArray(NonNullBulkString("sentinel") +: MastersRead(masters) +: Seq()) => - Some(Sentinel(masters)) - case _ => None + case Arr(Bulk("sentinel") +: MR(masters) +: Seq()) => Some(Sentinel(masters)) + case _ => None } } } final class Parameters(private val properties: Map[String, String]) extends AnyVal with Dynamic { - import RESP.err - - def selectDynamic[A](field: String)( - implicit R: String ==> A - ): Maybe[A] = properties.get(field).flatMap(R.read).toRight(err(s"no key $field of the provided type found")) + def selectDynamic[A](field: String)(implicit R: String ==> A): Maybe[A] = + properties.get(field).flatMap(R.read).toRight(Err(s"no key $field of the provided type found")) } final case class ConnectedClients(clients: Seq[Parameters]) final object ConnectedClients { private val KVPair = "(.*)=(.*)".r - implicit val connectedClientsRead: NonNullBulkString ==> ConnectedClients = Read.instancePF { - case NonNullBulkString(s) => - ConnectedClients { - s.split('\n').map { clientData => - new Parameters(clientData.split(' ').collect { case KVPair(k, v) => k -> v }.toMap) + + implicit val connectedClientsRead: Bulk ==> ConnectedClients = Read.instancePF { + case Bulk(s) => + ConnectedClients( + s.split(LF_CH).map { clientData => + new Parameters(clientData.split(SPACE_CH).collect { case KVPair(k, v) => k -> v }.toMap) } - } + ) } } final case class Configuration(parameters: Parameters) final object Configuration { - implicit val configRead: NonNilArray ==> Configuration = - Read[NonNilArray, Seq[(String, String)]].map(kvs => Configuration(new Parameters(kvs.toMap))) + implicit val configRead: Arr ==> Configuration = + Read[Arr, Seq[(String, String)]].map(kvs => Configuration(new Parameters(kvs.toMap))) } final case class Info(sections: Map[InfoSection, Parameters]) final object Info { - private val InfoSectionRead: String ==> InfoSection = Read.instancePF { + private val ISR: String ==> InfoSection = Read.instancePF { case "server" => InfoSection.server case "clients" => InfoSection.clients case "memory" => InfoSection.memory @@ -141,20 +131,22 @@ object ServerP { case "keyspace" => InfoSection.keyspace } private val KVPair = "(.*):(.*)".r - private val ParametersRead: Seq[String] ==> Parameters = Read.instancePF { + private val PR: Seq[String] ==> Parameters = Read.instancePF { case ss => new Parameters(ss.collect { case KVPair(k, v) => k -> v }.toMap) } - implicit val infoRead: NonNullBulkString ==> Info = Read.instancePF { - case NonNullBulkString(s) => - Info { - s.split("\n\n") - .flatMap(_.split("\n").toSeq match { - case InfoSectionRead(infoSection) +: ParametersRead(parameters) => Some(infoSection -> parameters) - case _ => None - }) + implicit val infoRead: Bulk ==> Info = Read.instancePF { + case Bulk(s) => + Info( + s.split(LF * 2) + .flatMap { + _.split(LF_CH).toSeq match { + case ISR(infoSection) +: PR(parameters) => Some(infoSection -> parameters) + case _ => None + } + } .toMap - } + ) } } } @@ -168,75 +160,68 @@ trait ServerP { final val flag = ShutdownFlag } - final val bgrewriteaof: Protocol.Aux[OK] = Protocol("BGREWRITEAOF", Nil).as[SimpleString, OK] + final val bgrewriteaof: Protocol.Aux[OK] = Protocol("BGREWRITEAOF", Nil).as[Str, OK] - final val bgsave: Protocol.Aux[OK] = Protocol("BGSAVE", Nil).as[SimpleString, OK] + final val bgsave: Protocol.Aux[OK] = Protocol("BGSAVE", Nil).as[Str, OK] final object client { - val getname: Protocol.Aux[Option[ConnectionName]] = - Protocol("CLIENT", "GETNAME").asC[NullBulkString :+: NonNullBulkString :+: CNil, Option[ConnectionName]] + import Show.{hostShow, portShow} + + val getname: Protocol.Aux[Option[ConnectionName]] = Protocol("CLIENT", "GETNAME").opt[GenBulk].as[ConnectionName] //FIXME other variations of kill def kill(host: Host, port: Port): Protocol.Aux[OK] = - Protocol("CLIENT", "KILL" :: s"${Show.hostShow.show(host)}:${Show.portShow.show(port)}" :: Nil) - .as[SimpleString, OK] + Protocol("CLIENT", "KILL" :: s"${hostShow.show(host)}:${portShow.show(port)}" :: Nil).as[Str, OK] - val list: Protocol.Aux[ConnectedClients] = - Protocol("CLIENT", "LIST").as[NonNullBulkString, ConnectedClients] + val list: Protocol.Aux[ConnectedClients] = Protocol("CLIENT", "LIST").as[Bulk, ConnectedClients] - def pause(milliseconds: PosLong): Protocol.Aux[OK] = - Protocol("CLIENT", "PAUSE" :: milliseconds :: HNil).as[SimpleString, OK] + def pause(milliseconds: PosLong): Protocol.Aux[OK] = Protocol("CLIENT", "PAUSE" :: milliseconds :: HNil).as[Str, OK] - def setname(connectionName: ConnectionName): Protocol.Aux[OK] = - Protocol("CLIENT", "SETNAME" :: connectionName.value :: Nil).as[SimpleString, OK] + def setname(connectionName: ConnectionName): Protocol.Aux[OK] = Protocol("CLIENT", "SETNAME" :: connectionName.value :: Nil).as[Str, OK] - val unsetname: Protocol.Aux[OK] = Protocol("CLIENT", "SETNAME" :: "" :: Nil).as[SimpleString, OK] + val unsetname: Protocol.Aux[OK] = Protocol("CLIENT", "SETNAME" :: "" :: Nil).as[Str, OK] } //TODO command? final object config { def get(parameter: GlobPattern): Protocol.Aux[Configuration] = - Protocol("CONFIG", "GET" :: parameter.value :: Nil).as[NonNilArray, Configuration] + Protocol("CONFIG", "GET" :: parameter.value :: Nil).as[Arr, Configuration] - val resetstat: Protocol.Aux[OK] = Protocol("CONFIG", "RESETSTAT").as[SimpleString, OK] + val resetstat: Protocol.Aux[OK] = Protocol("CONFIG", "RESETSTAT").as[Str, OK] - val rewrite: Protocol.Aux[OK] = Protocol("CONFIG", "REWRITE").as[SimpleString, OK] + val rewrite: Protocol.Aux[OK] = Protocol("CONFIG", "REWRITE").as[Str, OK] - def set[A: Show](parameter: Key, value: A): Protocol.Aux[OK] = - Protocol("CONFIG", "SET" :: parameter :: value :: HNil).as[SimpleString, OK] + def set[A: Show](parameter: Key, value: A): Protocol.Aux[OK] = Protocol("CONFIG", "SET" :: parameter :: value :: HNil).as[Str, OK] } - final val dbsize: Protocol.Aux[NonNegLong] = Protocol("DBSIZE", Nil).as[Integer, NonNegLong] - - final val flushall: Protocol.Aux[OK] = Protocol("FLUSHALL", Nil).as[SimpleString, OK] - - final val flushallasync: Protocol.Aux[OK] = Protocol("FLUSHALL", "ASYNC").as[SimpleString, OK] + final val dbsize: Protocol.Aux[NonNegLong] = Protocol("DBSIZE", Nil).as[Num, NonNegLong] - final val flushdb: Protocol.Aux[OK] = Protocol("FLUSHDB", Nil).as[SimpleString, OK] + final val flushall: Protocol.Aux[OK] = Protocol("FLUSHALL", Nil).as[Str, OK] - final val flushdbasync: Protocol.Aux[OK] = Protocol("FLUSHDB", "ASYNC").as[SimpleString, OK] + final val flushallasync: Protocol.Aux[OK] = Protocol("FLUSHALL", "ASYNC").as[Str, OK] - final val info: Protocol.Aux[Info] = info(servers.info.default) + final val flushdb: Protocol.Aux[OK] = Protocol("FLUSHDB", Nil).as[Str, OK] - final def info(section: InfoSection): Protocol.Aux[Info] = Protocol("INFO", section).as[NonNullBulkString, Info] + final val flushdbasync: Protocol.Aux[OK] = Protocol("FLUSHDB", "ASYNC").as[Str, OK] - final val lastsave: Protocol.Aux[NonNegLong] = Protocol("LASTSAVE", Nil).as[Integer, NonNegLong] + final val info: Protocol.Aux[Info] = info(servers.info.default) + final def info(section: InfoSection): Protocol.Aux[Info] = Protocol("INFO", section).as[Bulk, Info] - final val role: Protocol.Aux[Role] = Protocol("ROLE", Nil).as[NonNilArray, Role] + final val lastsave: Protocol.Aux[NonNegLong] = Protocol("LASTSAVE", Nil).as[Num, NonNegLong] - final val save: Protocol.Aux[OK] = Protocol("SAVE", Nil).as[SimpleString, OK] + final val role: Protocol.Aux[Role] = Protocol("ROLE", Nil).as[Arr, Role] - final val shutdown: Protocol.Aux[OK] = Protocol("SHUTDOWN", Nil).as[SimpleString, OK] + final val save: Protocol.Aux[OK] = Protocol("SAVE", Nil).as[Str, OK] - final def shutdown(flag: ShutdownFlag): Protocol.Aux[OK] = Protocol("SHUTDOWN", flag).as[SimpleString, OK] + final val shutdown: Protocol.Aux[OK] = Protocol("SHUTDOWN", Nil).as[Str, OK] + final def shutdown(flag: ShutdownFlag): Protocol.Aux[OK] = Protocol("SHUTDOWN", flag).as[Str, OK] - final def slaveof(host: Host, port: Port): Protocol.Aux[OK] = - Protocol("SLAVEOF", host :: port :: HNil).as[SimpleString, OK] + final def slaveof(host: Host, port: Port): Protocol.Aux[OK] = Protocol("SLAVEOF", host :: port :: HNil).as[Str, OK] - final val slaveofnoone: Protocol.Aux[OK] = Protocol("SLAVEOF", "NO" :: "ONE" :: Nil).as[SimpleString, OK] + final val slaveofnoone: Protocol.Aux[OK] = Protocol("SLAVEOF", "NO" :: "ONE" :: Nil).as[Str, OK] //TODO slowlog? sync? - final val time: Protocol.Aux[Time] = Protocol("TIME", Nil).as[NonNilArray, Time] + final val time: Protocol.Aux[Time] = Protocol("TIME", Nil).as[Arr, Time] } diff --git a/core/src/main/scala/laserdisc/protocol/SetP.scala b/core/src/main/scala/laserdisc/protocol/SetP.scala index d819f3fc..7dc79ca5 100644 --- a/core/src/main/scala/laserdisc/protocol/SetP.scala +++ b/core/src/main/scala/laserdisc/protocol/SetP.scala @@ -1,84 +1,57 @@ package laserdisc package protocol -trait SetP { - import Read.==> +trait SetBaseP { import shapeless._ - private[this] final val zeroIsNone = RESPRead.instance(Read.integerZeroIsNone[PosInt]) + private[this] final val zeroIsNone = RESPRead.instance(Read.numZeroIsNone[PosInt]) final def sadd[A: Show](key: Key, members: OneOrMore[A]): Protocol.Aux[NonNegInt] = - Protocol("SADD", key :: members.value :: HNil).as[Integer, NonNegInt] + Protocol("SADD", key :: members.value :: HNil).as[Num, NonNegInt] final def scard(key: Key): Protocol.Aux[Option[PosInt]] = Protocol("SCARD", key).using(zeroIsNone) - final def sdiff[A](keys: TwoOrMoreKeys)( - implicit ev0: NonNilArray ==> Seq[A] - ): Protocol.Aux[Seq[A]] = Protocol("SDIFF", keys.value).as[NonNilArray, Seq[A]] + final def sdiff[A: λ[a => Arr ==> Seq[a]]](keys: TwoOrMoreKeys): Protocol.Aux[Seq[A]] = Protocol("SDIFF", keys.value).as[Arr, Seq[A]] final def sdiffstore(keys: TwoOrMoreKeys, destinationKey: Key): Protocol.Aux[NonNegInt] = - Protocol("SDIFFSTORE", destinationKey :: keys.value).as[Integer, NonNegInt] + Protocol("SDIFFSTORE", destinationKey :: keys.value).as[Num, NonNegInt] - final def sinter[A](keys: TwoOrMoreKeys)( - implicit ev0: NonNilArray ==> Seq[A] - ): Protocol.Aux[Seq[A]] = Protocol("SINTER", keys.value).as[NonNilArray, Seq[A]] + final def sinter[A: λ[a => Arr ==> Seq[a]]](keys: TwoOrMoreKeys): Protocol.Aux[Seq[A]] = Protocol("SINTER", keys.value).as[Arr, Seq[A]] final def sinterstore(keys: TwoOrMoreKeys, destinationKey: Key): Protocol.Aux[NonNegInt] = - Protocol("SINTERSTORE", destinationKey :: keys.value).as[Integer, NonNegInt] + Protocol("SINTERSTORE", destinationKey :: keys.value).as[Num, NonNegInt] - final def sismember[A: Show](key: Key, member: A): Protocol.Aux[Boolean] = - Protocol("SISMEMBER", key :: member :: HNil).as[Integer, Boolean] + final def sismember[A: Show](key: Key, member: A): Protocol.Aux[Boolean] = Protocol("SISMEMBER", key :: member :: HNil).as[Num, Boolean] - final def smembers[A](key: Key)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[A]] = Protocol("SMEMBERS", key).as[NonNilArray, Seq[A]] + final def smembers[A: Bulk ==> ?](key: Key): Protocol.Aux[Seq[A]] = Protocol("SMEMBERS", key).as[Arr, Seq[A]] final def smove[A: Show](source: Key, destination: Key, member: A): Protocol.Aux[Boolean] = - Protocol("SMOVE", source :: destination :: member :: HNil).as[Integer, Boolean] + Protocol("SMOVE", source :: destination :: member :: HNil).as[Num, Boolean] - final def spop[A](key: Key)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Option[A]] = Protocol("SPOP", key).asC[NullBulkString :+: NonNullBulkString :+: CNil, Option[A]] + final def spop[A: Bulk ==> ?](key: Key): Protocol.Aux[Option[A]] = Protocol("SPOP", key).opt[GenBulk].as[A] + final def spop[A: Bulk ==> ?](key: Key, count: PosInt): Protocol.Aux[Seq[A]] = Protocol("SPOP", key :: count :: HNil).as[Arr, Seq[A]] - final def spop[A](key: Key, count: PosInt)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[A]] = Protocol("SPOP", key :: count :: HNil).as[NonNilArray, Seq[A]] - - final def srandmember[A](key: Key)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Option[A]] = - Protocol("SRANDMEMBER", key).asC[NullBulkString :+: NonNullBulkString :+: CNil, Option[A]] - - final def srandmembers[A](key: Key, count: NonZeroInt)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[A]] = Protocol("SRANDMEMBER", key :: count :: HNil).as[NonNilArray, Seq[A]] + final def srandmember[A: Bulk ==> ?](key: Key): Protocol.Aux[Option[A]] = + Protocol("SRANDMEMBER", key).opt[GenBulk].as[A] + final def srandmembers[A: Bulk ==> ?](key: Key, count: NonZeroInt): Protocol.Aux[Seq[A]] = + Protocol("SRANDMEMBER", key :: count :: HNil).as[Arr, Seq[A]] final def srem[A: Show](key: Key, members: OneOrMore[A]): Protocol.Aux[NonNegInt] = - Protocol("SREM", key :: members.value :: HNil).as[Integer, NonNegInt] - - final def sscan[A](key: Key, cursor: NonNegLong)( - implicit ev: NonNilArray ==> Seq[A] - ): Protocol.Aux[Scan[A]] = Protocol("SSCAN", key :: cursor :: HNil).as[NonNilArray, Scan[A]] - - final def sscan[A](key: Key, cursor: NonNegLong, pattern: GlobPattern)( - implicit ev: NonNilArray ==> Seq[A] - ): Protocol.Aux[Scan[A]] = Protocol("SSCAN", key :: cursor :: "MATCH" :: pattern :: HNil).as[NonNilArray, Scan[A]] - - final def sscan[A](key: Key, cursor: NonNegLong, count: PosInt)( - implicit ev: NonNilArray ==> Seq[A] - ): Protocol.Aux[Scan[A]] = Protocol("SSCAN", key :: cursor :: "COUNT" :: count :: HNil).as[NonNilArray, Scan[A]] + Protocol("SREM", key :: members.value :: HNil).as[Num, NonNegInt] - final def sscan[A](key: Key, cursor: NonNegLong, pattern: GlobPattern, count: PosInt)( - implicit ev: NonNilArray ==> Seq[A] - ): Protocol.Aux[Scan[A]] = - Protocol("SSCAN", key :: cursor :: "MATCH" :: pattern :: "COUNT" :: count :: HNil).as[NonNilArray, Scan[A]] + final def sscan[A: λ[a => Arr ==> Seq[a]]](key: Key, cursor: NonNegLong): Protocol.Aux[Scan[A]] = + Protocol("SSCAN", key :: cursor :: HNil).as[Arr, Scan[A]] + final def sscan[A: λ[a => Arr ==> Seq[a]]](key: Key, cursor: NonNegLong, pattern: GlobPattern): Protocol.Aux[Scan[A]] = + Protocol("SSCAN", key :: cursor :: "MATCH" :: pattern :: HNil).as[Arr, Scan[A]] + final def sscan[A: λ[a => Arr ==> Seq[a]]](key: Key, cursor: NonNegLong, count: PosInt): Protocol.Aux[Scan[A]] = + Protocol("SSCAN", key :: cursor :: "COUNT" :: count :: HNil).as[Arr, Scan[A]] + final def sscan[A: λ[a => Arr ==> Seq[a]]](key: Key, cursor: NonNegLong, pattern: GlobPattern, count: PosInt): Protocol.Aux[Scan[A]] = + Protocol("SSCAN", key :: cursor :: "MATCH" :: pattern :: "COUNT" :: count :: HNil).as[Arr, Scan[A]] - final def sunion[A](keys: TwoOrMoreKeys)( - implicit ev0: NonNilArray ==> Seq[A] - ): Protocol.Aux[Seq[A]] = Protocol("SUNION", keys.value).as[NonNilArray, Seq[A]] + final def sunion[A: λ[a => Arr ==> Seq[a]]](keys: TwoOrMoreKeys): Protocol.Aux[Seq[A]] = Protocol("SUNION", keys.value).as[Arr, Seq[A]] final def sunionstore(keys: TwoOrMoreKeys, destinationKey: Key): Protocol.Aux[NonNegInt] = - Protocol("SUNIONSTORE", destinationKey :: keys.value).as[Integer, NonNegInt] + Protocol("SUNIONSTORE", destinationKey :: keys.value).as[Num, NonNegInt] } -trait AllSetP extends SetP with SetPExtra +trait SetP extends SetBaseP with SetExtP diff --git a/core/src/main/scala/laserdisc/protocol/Show.scala b/core/src/main/scala/laserdisc/protocol/Show.scala index 9dfaf0a4..44dc6e04 100644 --- a/core/src/main/scala/laserdisc/protocol/Show.scala +++ b/core/src/main/scala/laserdisc/protocol/Show.scala @@ -6,16 +6,22 @@ import eu.timepit.refined.api.Refined import scala.annotation.implicitNotFound @implicitNotFound( - "Implicit not found: Show[${A}].\n\n" + - "Try writing your own, for example:\n\n" + - "implicit final val myShow: Show[${A}] = new Show[${A}] {\n" + - " override final def show(a: ${A}): String = ???\n" + - "}\n" + """Implicit not found Show[${A}]. + +Try writing your own, for example: + +implicit final val myShow: Show[${A}] = new Show[${A}] { + override final def show(a: ${A}): String = ??? +} +""" ) trait Show[A] { + def show(a: A): String + + final def contramap[B](f: B => A): Show[B] = Show.instance(show _ compose f) } -object Show extends LowPriorityShowInstances { +object Show extends ShowInstances { @inline final def apply[A](implicit instance: Show[A]): Show[A] = instance final def const[A](s: => String): Show[A] = new Show[A] { @@ -29,7 +35,7 @@ object Show extends LowPriorityShowInstances { } } -trait LowPriorityShowInstances { +private[protocol] sealed trait ShowInstances { private[this] final val refinedDoubleCases: PartialFunction[Refined[Double, _], String] = { case d if d.value == Double.NegativeInfinity => "-inf" case d if d.value == Double.PositiveInfinity => "+inf" @@ -48,6 +54,10 @@ trait LowPriorityShowInstances { implicit final val hostShow: Show[Host] = Show.unsafeFromToString implicit final val indexShow: Show[Index] = Show.unsafeFromToString implicit final val keyShow: Show[Key] = Show.unsafeFromToString + implicit final val latitudeShow: Show[Latitude] = Show.unsafeFromToString + implicit final val longitudeShow: Show[Longitude] = Show.unsafeFromToString + implicit final val nodeIdShow: Show[NodeId] = Show.unsafeFromToString + implicit final val nonNegDoubleShow: Show[NonNegDouble] = Show.instance(refinedDoubleCases) implicit final val nonNegIntShow: Show[NonNegInt] = Show.unsafeFromToString implicit final val nonNegLongShow: Show[NonNegLong] = Show.unsafeFromToString implicit final val nonZeroDoubleShow: Show[NonZeroDouble] = Show.instance(refinedDoubleCases) @@ -57,6 +67,7 @@ trait LowPriorityShowInstances { implicit final val posIntShow: Show[PosInt] = Show.unsafeFromToString implicit final val posLongShow: Show[PosLong] = Show.unsafeFromToString implicit final val rangeOffsetShow: Show[RangeOffset] = Show.unsafeFromToString + implicit final val slotShow: Show[Slot] = Show.unsafeFromToString implicit final val stringLengthShow: Show[StringLength] = Show.unsafeFromToString implicit final val validDoubleShow: Show[ValidDouble] = Show.instance(refinedDoubleCases) } diff --git a/core/src/main/scala/laserdisc/protocol/SortedSetP.scala b/core/src/main/scala/laserdisc/protocol/SortedSetP.scala index 24890a59..6a28fc66 100644 --- a/core/src/main/scala/laserdisc/protocol/SortedSetP.scala +++ b/core/src/main/scala/laserdisc/protocol/SortedSetP.scala @@ -33,31 +33,21 @@ object SortedSetP { private[this] final val posInf = "+" private[this] final val negInf = "-" - sealed abstract class Open private[LexRange] ( - private[this] val minimum: String, - private[this] val maximum: String - ) extends LexRange { + sealed abstract class Open private[LexRange] (private[this] val minimum: String, private[this] val maximum: String) extends LexRange { override final val min: String = open(minimum) override final val max: String = open(maximum) } - sealed abstract class OpenClosed private[LexRange] ( - private[this] val minimum: String, - private[this] val maximum: String - ) extends LexRange { + sealed abstract class OpenClosed private[LexRange] (private[this] val minimum: String, private[this] val maximum: String) + extends LexRange { override final val min: String = open(minimum) override final val max: String = close(maximum) } - sealed abstract class ClosedOpen private[LexRange] ( - private[this] val minimum: String, - private[this] val maximum: String - ) extends LexRange { + sealed abstract class ClosedOpen private[LexRange] (private[this] val minimum: String, private[this] val maximum: String) + extends LexRange { override final val min: String = close(minimum) override final val max: String = open(maximum) } - sealed abstract class Closed private[LexRange] ( - private[this] val minimum: String, - private[this] val maximum: String - ) extends LexRange { + sealed abstract class Closed private[LexRange] (private[this] val minimum: String, private[this] val maximum: String) extends LexRange { override final val min: String = close(minimum) override final val max: String = close(maximum) } @@ -90,34 +80,28 @@ object SortedSetP { sealed trait ScoreRange { def min: String; def max: String } final object ScoreRange { - private[this] def open(d: ValidDouble) = s"(${Show.validDoubleShow.show(d)}" - private[this] def close(d: ValidDouble) = Show.validDoubleShow.show(d) + import Show.validDoubleShow - sealed abstract class Open private[ScoreRange] ( - private[this] val minimum: ValidDouble, - private[this] val maximum: ValidDouble - ) extends ScoreRange { + private[this] def open(d: ValidDouble) = s"(${validDoubleShow.show(d)}" + private[this] def close(d: ValidDouble) = validDoubleShow.show(d) + + sealed abstract class Open private[ScoreRange] (private[this] val minimum: ValidDouble, private[this] val maximum: ValidDouble) + extends ScoreRange { override final val min: String = open(minimum) override final val max: String = open(maximum) } - sealed abstract class OpenClosed private[ScoreRange] ( - private[this] val minimum: ValidDouble, - private[this] val maximum: ValidDouble - ) extends ScoreRange { + sealed abstract class OpenClosed private[ScoreRange] (private[this] val minimum: ValidDouble, private[this] val maximum: ValidDouble) + extends ScoreRange { override final val min: String = open(minimum) override final val max: String = close(maximum) } - sealed abstract class ClosedOpen private[ScoreRange] ( - private[this] val minimum: ValidDouble, - private[this] val maximum: ValidDouble - ) extends ScoreRange { + sealed abstract class ClosedOpen private[ScoreRange] (private[this] val minimum: ValidDouble, private[this] val maximum: ValidDouble) + extends ScoreRange { override final val min: String = close(minimum) override final val max: String = open(maximum) } - sealed abstract class Closed private[ScoreRange] ( - private[this] val minimum: ValidDouble, - private[this] val maximum: ValidDouble - ) extends ScoreRange { + sealed abstract class Closed private[ScoreRange] (private[this] val minimum: ValidDouble, private[this] val maximum: ValidDouble) + extends ScoreRange { override final val min: String = close(minimum) override final val max: String = close(maximum) } @@ -129,13 +113,12 @@ object SortedSetP { } } -trait SortedSetP { +trait SortedSetBaseP { import SortedSetP.{Aggregate, Flag, LexRange, ScoreRange} - import Read.==> import auto._ import shapeless._ - private[this] final val zeroIsNone = RESPRead.instance(Read.integerZeroIsNone[PosInt]) + private[this] final val zeroIsNone = RESPRead.instance(Read.numZeroIsNone[PosInt]) final object sortedsets { final val aggregate = Aggregate @@ -145,197 +128,148 @@ trait SortedSetP { } final def zadd[A: Show](key: Key, scoredMembers: OneOrMore[(A, ValidDouble)]): Protocol.Aux[NonNegInt] = - Protocol("ZADD", key :: scoredMembers.value.map(_.swap) :: HNil).as[Integer, NonNegInt] - + Protocol("ZADD", key :: scoredMembers.map(_.swap) :: HNil).as[Num, NonNegInt] final def zadd[A: Show](key: Key, flag: Flag, scoredMembers: OneOrMore[(A, ValidDouble)]): Protocol.Aux[NonNegInt] = - Protocol("ZADD", key :: flag :: scoredMembers.map(_.swap) :: HNil).as[Integer, NonNegInt] + Protocol("ZADD", key :: flag :: scoredMembers.map(_.swap) :: HNil).as[Num, NonNegInt] final def zaddch[A: Show](key: Key, scoredMembers: OneOrMore[(A, ValidDouble)]): Protocol.Aux[NonNegInt] = - Protocol("ZADD", key :: "CH" :: scoredMembers.map(_.swap) :: HNil).as[Integer, NonNegInt] - + Protocol("ZADD", key :: "CH" :: scoredMembers.map(_.swap) :: HNil).as[Num, NonNegInt] final def zaddch[A: Show](key: Key, flag: Flag, scoredMembers: OneOrMore[(A, ValidDouble)]): Protocol.Aux[NonNegInt] = - Protocol("ZADD", key :: flag :: "CH" :: scoredMembers.map(_.swap) :: HNil).as[Integer, NonNegInt] + Protocol("ZADD", key :: flag :: "CH" :: scoredMembers.map(_.swap) :: HNil).as[Num, NonNegInt] final def zaddincr[A: Show](key: Key, member: A, increment: NonZeroDouble): Protocol.Aux[Double] = - Protocol("ZADD", key :: increment :: member :: HNil).as[NonNullBulkString, Double] - + Protocol("ZADD", key :: increment :: member :: HNil).as[Bulk, Double] final def zaddincr[A: Show](key: Key, flag: Flag, member: A, increment: NonZeroDouble): Protocol.Aux[Double] = - Protocol("ZADD", key :: flag :: increment :: member :: HNil).as[NonNullBulkString, Double] + Protocol("ZADD", key :: flag :: increment :: member :: HNil).as[Bulk, Double] final def zaddchincr[A: Show](key: Key, member: A, increment: NonZeroDouble): Protocol.Aux[Double] = - Protocol("ZADD", key :: "CH" :: "INCR" :: increment :: member :: HNil).as[NonNullBulkString, Double] - + Protocol("ZADD", key :: "CH" :: "INCR" :: increment :: member :: HNil).as[Bulk, Double] final def zaddchincr[A: Show](key: Key, flag: Flag, member: A, increment: NonZeroDouble): Protocol.Aux[Double] = - Protocol("ZADD", key :: flag :: "CH" :: "INCR" :: increment :: member :: HNil).as[NonNullBulkString, Double] + Protocol("ZADD", key :: flag :: "CH" :: "INCR" :: increment :: member :: HNil).as[Bulk, Double] final def zcard(key: Key): Protocol.Aux[Option[PosInt]] = Protocol("ZCARD", key :: HNil).using(zeroIsNone) final def zcount(key: Key, range: ScoreRange): Protocol.Aux[NonNegInt] = - Protocol("ZCOUNT", key :: range.min :: range.max :: HNil).as[Integer, NonNegInt] + Protocol("ZCOUNT", key :: range.min :: range.max :: HNil).as[Num, NonNegInt] final def zincrby[A: Show](key: Key, member: A, increment: ValidDouble): Protocol.Aux[Double] = - Protocol("ZINCRBY", key :: increment :: member :: HNil).as[NonNullBulkString, Double] + Protocol("ZINCRBY", key :: increment :: member :: HNil).as[Bulk, Double] final def zinterstore(keys: TwoOrMoreKeys, destinationKey: Key): Protocol.Aux[NonNegInt] = - Protocol("ZINTERSTORE", destinationKey :: keys.length :: keys.value :: HNil).as[Integer, NonNegInt] + Protocol("ZINTERSTORE", destinationKey :: keys.length :: keys.value :: HNil).as[Num, NonNegInt] final def zinterstoreweighted(weightedKeys: TwoOrMoreWeightedKeys, destinationKey: Key): Protocol.Aux[NonNegInt] = { val (keys, weights) = weightedKeys.unzip - Protocol("ZINTERSTORE", destinationKey :: keys.length :: keys :: "WEIGHTS" :: weights :: HNil) - .as[Integer, NonNegInt] + Protocol("ZINTERSTORE", destinationKey :: keys.length :: keys :: "WEIGHTS" :: weights :: HNil).as[Num, NonNegInt] } final def zinterstore(keys: TwoOrMoreKeys, destinationKey: Key, aggregate: Aggregate): Protocol.Aux[NonNegInt] = - Protocol("ZINTERSTORE", destinationKey :: keys.length :: keys.value :: "AGGREGATE" :: aggregate :: HNil) - .as[Integer, NonNegInt] - - final def zinterstoreweighted( - weightedKeys: TwoOrMoreWeightedKeys, - destinationKey: Key, - aggregate: Aggregate - ): Protocol.Aux[NonNegInt] = { + Protocol("ZINTERSTORE", destinationKey :: keys.length :: keys.value :: "AGGREGATE" :: aggregate :: HNil).as[Num, NonNegInt] + + final def zinterstoreweighted(weightedKeys: TwoOrMoreWeightedKeys, destinationKey: Key, aggregate: Aggregate): Protocol.Aux[NonNegInt] = { val (keys, weights) = weightedKeys.unzip - Protocol( - "ZINTERSTORE", - destinationKey :: keys.length :: keys :: "WEIGHTS" :: weights :: "AGGREGATE" :: aggregate :: HNil - ).as[Integer, NonNegInt] + Protocol("ZINTERSTORE", destinationKey :: keys.length :: keys :: "WEIGHTS" :: weights :: "AGGREGATE" :: aggregate :: HNil) + .as[Num, NonNegInt] } final def zlexcount(key: Key, range: LexRange): Protocol.Aux[NonNegInt] = - Protocol("ZLEXCOUNT", key :: range.min :: range.max :: HNil).as[Integer, NonNegInt] - - final def zrange[A](key: Key, start: Index, stop: Index)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[A]] = Protocol("ZRANGE", key :: start :: stop :: HNil).as[NonNilArray, Seq[A]] - - final def zrangebylex[A](key: Key, range: LexRange)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[A]] = Protocol("ZRANGEBYLEX", key :: range.min :: range.max :: HNil).as[NonNilArray, Seq[A]] + Protocol("ZLEXCOUNT", key :: range.min :: range.max :: HNil).as[Num, NonNegInt] - final def zrangebylex[A](key: Key, range: LexRange, offset: NonNegLong, count: PosLong)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[A]] = - Protocol("ZRANGEBYLEX", key :: range.min :: range.max :: "LIMIT" :: offset :: count :: HNil).as[NonNilArray, Seq[A]] + final def zrange[A: Bulk ==> ?](key: Key, start: Index, stop: Index): Protocol.Aux[Seq[A]] = + Protocol("ZRANGE", key :: start :: stop :: HNil).as[Arr, Seq[A]] - final def zrangebyscore[A](key: Key, range: ScoreRange)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[A]] = Protocol("ZRANGEBYSCORE", key :: range.min :: range.max :: HNil).as[NonNilArray, Seq[A]] + final def zrangebylex[A: Bulk ==> ?](key: Key, range: LexRange): Protocol.Aux[Seq[A]] = + Protocol("ZRANGEBYLEX", key :: range.min :: range.max :: HNil).as[Arr, Seq[A]] - final def zrangebyscore[A](key: Key, range: ScoreRange, offset: NonNegLong, count: PosLong)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[A]] = - Protocol("ZRANGEBYSCORE", key :: range.min :: range.max :: "LIMIT" :: offset :: count :: HNil) - .as[NonNilArray, Seq[A]] + final def zrangebylex[A: Bulk ==> ?](key: Key, range: LexRange, offset: NonNegLong, count: PosLong): Protocol.Aux[Seq[A]] = + Protocol("ZRANGEBYLEX", key :: range.min :: range.max :: "LIMIT" :: offset :: count :: HNil).as[Arr, Seq[A]] - final def zrangebyscorewithscores[A](key: Key, range: ScoreRange)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[(A, Double)]] = - Protocol("ZRANGEBYSCORE", key :: range.min :: range.max :: "WITHSCORES" :: HNil).as[NonNilArray, Seq[(A, Double)]] + final def zrangebyscore[A: Bulk ==> ?](key: Key, range: ScoreRange): Protocol.Aux[Seq[A]] = + Protocol("ZRANGEBYSCORE", key :: range.min :: range.max :: HNil).as[Arr, Seq[A]] + final def zrangebyscore[A: Bulk ==> ?](key: Key, range: ScoreRange, offset: NonNegLong, count: PosLong): Protocol.Aux[Seq[A]] = + Protocol("ZRANGEBYSCORE", key :: range.min :: range.max :: "LIMIT" :: offset :: count :: HNil).as[Arr, Seq[A]] - final def zrangebyscorewithscores[A](key: Key, range: ScoreRange, offset: NonNegLong, count: PosLong)( - implicit ev: NonNullBulkString ==> A + final def zrangebyscorewithscores[A: Bulk ==> ?](key: Key, range: ScoreRange): Protocol.Aux[Seq[(A, Double)]] = + Protocol("ZRANGEBYSCORE", key :: range.min :: range.max :: "WITHSCORES" :: HNil).as[Arr, Seq[(A, Double)]] + final def zrangebyscorewithscores[A: Bulk ==> ?]( + key: Key, + range: ScoreRange, + offset: NonNegLong, + count: PosLong ): Protocol.Aux[Seq[(A, Double)]] = - Protocol("ZRANGEBYSCORE", key :: range.min :: range.max :: "WITHSCORES" :: "LIMIT" :: offset :: count :: HNil) - .as[NonNilArray, Seq[(A, Double)]] + Protocol("ZRANGEBYSCORE", key :: range.min :: range.max :: "WITHSCORES" :: "LIMIT" :: offset :: count :: HNil).as[Arr, Seq[(A, Double)]] final def zrank(key: Key, member: Key): Protocol.Aux[Option[NonNegInt]] = - Protocol("ZRANK", key :: member :: Nil).asC[NullBulkString :+: Integer :+: CNil, Option[NonNegInt]] + Protocol("ZRANK", key :: member :: Nil).asC[Num :+: NullBulk :+: CNil, Option[NonNegInt]] final def zrem[A: Show](key: Key, members: OneOrMore[A]): Protocol.Aux[NonNegInt] = - Protocol("ZREM", key :: members.value :: HNil).as[Integer, NonNegInt] + Protocol("ZREM", key :: members.value :: HNil).as[Num, NonNegInt] final def zremrangebylex(key: Key, range: LexRange): Protocol.Aux[NonNegInt] = - Protocol("ZREMRANGEBYLEX", key :: range.min :: range.max :: HNil).as[Integer, NonNegInt] + Protocol("ZREMRANGEBYLEX", key :: range.min :: range.max :: HNil).as[Num, NonNegInt] final def zremrangebyrank(key: Key, start: Index, stop: Index): Protocol.Aux[NonNegInt] = - Protocol("ZREMRANGEBYRANK", key :: start :: stop :: HNil).as[Integer, NonNegInt] + Protocol("ZREMRANGEBYRANK", key :: start :: stop :: HNil).as[Num, NonNegInt] final def zremrangebyscore(key: Key, range: ScoreRange): Protocol.Aux[NonNegInt] = - Protocol("ZREMRANGEBYSCORE", key :: range.min :: range.max :: HNil).as[Integer, NonNegInt] - - final def zrevrange[A](key: Key, start: Index, stop: Index)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[A]] = Protocol("ZREVRANGE", key :: start :: stop :: HNil).as[NonNilArray, Seq[A]] - - final def zrevrangebylex[A](key: Key, range: LexRange)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[A]] = Protocol("ZREVRANGEBYLEX", key :: range.max :: range.min :: HNil).as[NonNilArray, Seq[A]] - - final def zrevrangebylex[A](key: Key, range: LexRange, offset: NonNegLong, count: PosLong)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[A]] = - Protocol("ZREVRANGEBYLEX", key :: range.max :: range.min :: "LIMIT" :: offset :: count :: HNil) - .as[NonNilArray, Seq[A]] - - final def zrevrangebyscore[A](key: Key, range: ScoreRange)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[A]] = Protocol("ZREVRANGEBYSCORE", key :: range.max :: range.min :: HNil).as[NonNilArray, Seq[A]] - - final def zrevrangebyscore[A](key: Key, range: ScoreRange, offset: NonNegLong, count: PosLong)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[A]] = - Protocol("ZREVRANGEBYSCORE", key :: range.max :: range.min :: "LIMIT" :: offset :: count :: HNil) - .as[NonNilArray, Seq[A]] - - final def zrevrangebyscorewithscores[A](key: Key, range: ScoreRange)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[(A, Double)]] = - Protocol("ZREVRANGEBYSCORE", key :: range.max :: range.min :: "WITHSCORES" :: HNil) - .as[NonNilArray, Seq[(A, Double)]] + Protocol("ZREMRANGEBYSCORE", key :: range.min :: range.max :: HNil).as[Num, NonNegInt] - final def zrevrangebyscorewithscores[A](key: Key, range: ScoreRange, offset: NonNegLong, count: PosLong)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Seq[(A, Double)]] = - Protocol("ZREVRANGEBYSCORE", key :: range.max :: range.min :: "WITHSCORES" :: "LIMIT" :: offset :: count :: HNil) - .as[NonNilArray, Seq[(A, Double)]] + final def zrevrange[A: Bulk ==> ?](key: Key, start: Index, stop: Index): Protocol.Aux[Seq[A]] = + Protocol("ZREVRANGE", key :: start :: stop :: HNil).as[Arr, Seq[A]] - final def zrevrank(key: NonNullBulkString, member: Key): Protocol.Aux[Option[NonNegInt]] = - Protocol("ZREVRANK", key :: member :: HNil).asC[NullBulkString :+: Integer :+: CNil, Option[NonNegInt]] + final def zrevrangebylex[A: Bulk ==> ?](key: Key, range: LexRange): Protocol.Aux[Seq[A]] = + Protocol("ZREVRANGEBYLEX", key :: range.max :: range.min :: HNil).as[Arr, Seq[A]] - final def zscan[A](key: Key, cursor: NonNegLong)( - implicit ev: NonNilArray ==> Seq[A] - ): Protocol.Aux[Scan[A]] = Protocol("ZSCAN", key :: cursor :: HNil).as[NonNilArray, Scan[A]] + final def zrevrangebylex[A: Bulk ==> ?](key: Key, range: LexRange, offset: NonNegLong, count: PosLong): Protocol.Aux[Seq[A]] = + Protocol("ZREVRANGEBYLEX", key :: range.max :: range.min :: "LIMIT" :: offset :: count :: HNil).as[Arr, Seq[A]] - final def zscan[A](key: Key, cursor: NonNegLong, pattern: GlobPattern)( - implicit ev: NonNilArray ==> Seq[A] - ): Protocol.Aux[Scan[A]] = Protocol("ZSCAN", key :: cursor :: "MATCH" :: pattern :: HNil).as[NonNilArray, Scan[A]] + final def zrevrangebyscore[A: Bulk ==> ?](key: Key, range: ScoreRange): Protocol.Aux[Seq[A]] = + Protocol("ZREVRANGEBYSCORE", key :: range.max :: range.min :: HNil).as[Arr, Seq[A]] + final def zrevrangebyscore[A: Bulk ==> ?](key: Key, range: ScoreRange, offset: NonNegLong, count: PosLong): Protocol.Aux[Seq[A]] = + Protocol("ZREVRANGEBYSCORE", key :: range.max :: range.min :: "LIMIT" :: offset :: count :: HNil).as[Arr, Seq[A]] - final def zscan[A](key: Key, cursor: NonNegLong, count: PosInt)( - implicit ev: NonNilArray ==> Seq[A] - ): Protocol.Aux[Scan[A]] = Protocol("ZSCAN", key :: cursor :: "COUNT" :: count :: HNil).as[NonNilArray, Scan[A]] + final def zrevrangebyscorewithscores[A: Bulk ==> ?](key: Key, range: ScoreRange): Protocol.Aux[Seq[(A, Double)]] = + Protocol("ZREVRANGEBYSCORE", key :: range.max :: range.min :: "WITHSCORES" :: HNil).as[Arr, Seq[(A, Double)]] + final def zrevrangebyscorewithscores[A: Bulk ==> ?]( + key: Key, + range: ScoreRange, + offset: NonNegLong, + count: PosLong + ): Protocol.Aux[Seq[(A, Double)]] = + Protocol("ZREVRANGEBYSCORE", key :: range.max :: range.min :: "WITHSCORES" :: "LIMIT" :: offset :: count :: HNil) + .as[Arr, Seq[(A, Double)]] - final def zscan[A](key: Key, cursor: NonNegLong, pattern: GlobPattern, count: PosInt)( - implicit ev: NonNilArray ==> Seq[A] - ): Protocol.Aux[Scan[A]] = - Protocol("ZSCAN", key :: cursor :: "MATCH" :: pattern :: "COUNT" :: count :: HNil).as[NonNilArray, Scan[A]] + final def zrevrank(key: Bulk, member: Key): Protocol.Aux[Option[NonNegInt]] = + Protocol("ZREVRANK", key :: member :: HNil).asC[Num :+: NullBulk :+: CNil, Option[NonNegInt]] + + final def zscan[A: λ[a => Arr ==> Seq[a]]](key: Key, cursor: NonNegLong): Protocol.Aux[Scan[A]] = + Protocol("ZSCAN", key :: cursor :: HNil).as[Arr, Scan[A]] + final def zscan[A: λ[a => Arr ==> Seq[a]]](key: Key, cursor: NonNegLong, pattern: GlobPattern): Protocol.Aux[Scan[A]] = + Protocol("ZSCAN", key :: cursor :: "MATCH" :: pattern :: HNil).as[Arr, Scan[A]] + final def zscan[A: λ[a => Arr ==> Seq[a]]](key: Key, cursor: NonNegLong, count: PosInt): Protocol.Aux[Scan[A]] = + Protocol("ZSCAN", key :: cursor :: "COUNT" :: count :: HNil).as[Arr, Scan[A]] + final def zscan[A: λ[a => Arr ==> Seq[a]]](key: Key, cursor: NonNegLong, pattern: GlobPattern, count: PosInt): Protocol.Aux[Scan[A]] = + Protocol("ZSCAN", key :: cursor :: "MATCH" :: pattern :: "COUNT" :: count :: HNil).as[Arr, Scan[A]] final def zscore[A: Show](key: Key, member: A): Protocol.Aux[Option[Double]] = - Protocol("ZSCORE", key :: member :: HNil).asC[NullBulkString :+: NonNullBulkString :+: CNil, Option[Double]] + Protocol("ZSCORE", key :: member :: HNil).opt[GenBulk].as[Double] final def zunionstore(keys: TwoOrMoreKeys, destinationKey: Key): Protocol.Aux[NonNegInt] = - Protocol("ZUNIONSTORE", destinationKey :: keys.length :: keys.value :: HNil).as[Integer, NonNegInt] + Protocol("ZUNIONSTORE", destinationKey :: keys.length :: keys.value :: HNil).as[Num, NonNegInt] final def zunionstoreweighted(weightedKeys: TwoOrMoreWeightedKeys, destinationKey: Key): Protocol.Aux[NonNegInt] = { val (keys, weights) = weightedKeys.unzip - Protocol("ZUNIONSTORE", destinationKey :: keys.length :: keys :: "WEIGHTS" :: weights :: HNil) - .as[Integer, NonNegInt] + Protocol("ZUNIONSTORE", destinationKey :: keys.length :: keys :: "WEIGHTS" :: weights :: HNil).as[Num, NonNegInt] } final def zunionstore(keys: TwoOrMoreKeys, destinationKey: Key, aggregate: Aggregate): Protocol.Aux[NonNegInt] = - Protocol("ZUNIONSTORE", destinationKey :: keys.length :: keys.value :: "AGGREGATE" :: aggregate :: HNil) - .as[Integer, NonNegInt] - - final def zunionstoreweighted( - weightedKeys: TwoOrMoreWeightedKeys, - destinationKey: Key, - aggregate: Aggregate - ): Protocol.Aux[NonNegInt] = { + Protocol("ZUNIONSTORE", destinationKey :: keys.length :: keys.value :: "AGGREGATE" :: aggregate :: HNil).as[Num, NonNegInt] + + final def zunionstoreweighted(weightedKeys: TwoOrMoreWeightedKeys, destinationKey: Key, aggregate: Aggregate): Protocol.Aux[NonNegInt] = { val (keys, weights) = weightedKeys.unzip - Protocol( - "ZUNIONSTORE", - destinationKey :: keys.length :: keys :: "WEIGHTS" :: weights :: "AGGREGATE" :: aggregate :: HNil - ).as[Integer, NonNegInt] + Protocol("ZUNIONSTORE", destinationKey :: keys.length :: keys :: "WEIGHTS" :: weights :: "AGGREGATE" :: aggregate :: HNil) + .as[Num, NonNegInt] } } -trait AllSortedSetP extends SortedSetP with SortedSetPExtra +trait SortedSetP extends SortedSetBaseP with SortedSetExtP diff --git a/core/src/main/scala/laserdisc/protocol/StringP.scala b/core/src/main/scala/laserdisc/protocol/StringP.scala index 3b96e383..ef1c5319 100644 --- a/core/src/main/scala/laserdisc/protocol/StringP.scala +++ b/core/src/main/scala/laserdisc/protocol/StringP.scala @@ -2,8 +2,6 @@ package laserdisc package protocol object StringP { - import Read.==> - sealed trait Bit final object Bit { final object set extends Bit @@ -13,7 +11,7 @@ object StringP { case `set` => "1" case `unset` => "0" } - implicit val integer2BitRead: Integer ==> Bit = Read[Integer, Boolean].map(flag => if (flag) set else unset) + implicit val num2BitRead: Num ==> Bit = Read[Num, Boolean].map(flag => if (flag) set else unset) } sealed trait Bitwise @@ -65,24 +63,20 @@ object StringP { final class PartiallyAppliedGetSet[A](private val dummy: Boolean) extends AnyVal { import shapeless._ - - def apply[B: Show](key: Key, value: B)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Option[A]] = - Protocol("GETSET", key :: value :: HNil).asC[NullBulkString :+: NonNullBulkString :+: CNil, Option[A]] + def apply[B: Show](key: Key, value: B)(implicit ev: Bulk ==> A): Protocol.Aux[Option[A]] = + Protocol("GETSET", key :: value :: HNil).asC[Bulk :+: NullBulk :+: CNil, Option[A]] } } -trait StringP { +trait StringBaseP { import StringP.{Bit, Bitwise, Expiry, Flag, PartiallyAppliedGetSet} - import Read.==> import shapeless._ import shapeless.labelled.FieldType import shapeless.nat._ import shapeless.ops.hlist.Length import shapeless.ops.nat.GTEq.>= - private[this] final val minusOneIsNone = RESPRead.instance(Read.integerMinusOneIsNone[NonNegInt]) + private[this] final val minusOneIsNone = RESPRead.instance(Read.numMinusOneIsNone[NonNegInt]) final object strings { final val bit = Bit @@ -91,92 +85,68 @@ trait StringP { final val flag = Flag } - final def append[A: Show](key: Key, value: A): Protocol.Aux[NonNegInt] = - Protocol("APPEND", key :: value :: HNil).as[Integer, NonNegInt] - - final def bitcount(key: Key): Protocol.Aux[NonNegInt] = Protocol("BITCOUNT", key).as[Integer, NonNegInt] + final def append[A: Show](key: Key, value: A): Protocol.Aux[NonNegInt] = Protocol("APPEND", key :: value :: HNil).as[Num, NonNegInt] + final def bitcount(key: Key): Protocol.Aux[NonNegInt] = Protocol("BITCOUNT", key).as[Num, NonNegInt] final def bitcount(key: Key, start: Index, end: Index): Protocol.Aux[NonNegInt] = - Protocol("BITCOUNT", key :: start :: end :: HNil).as[Integer, NonNegInt] + Protocol("BITCOUNT", key :: start :: end :: HNil).as[Num, NonNegInt] //FIXME add BITFIELD final def bitop(bitwise: Bitwise, keys: TwoOrMoreKeys, destinationKey: Key): Protocol.Aux[NonNegInt] = - Protocol("BITOP", bitwise :: destinationKey :: keys.value :: HNil).as[Integer, NonNegInt] + Protocol("BITOP", bitwise :: destinationKey :: keys.value :: HNil).as[Num, NonNegInt] final def bitopnot(key: Key, destinationKey: Key): Protocol.Aux[NonNegInt] = - Protocol("BITOP", "NOT" :: destinationKey.value :: key.value :: Nil).as[Integer, NonNegInt] - - final def bitpos(key: Key, bit: Bit): Protocol.Aux[Option[NonNegInt]] = - Protocol("BITPOS", key :: bit :: HNil).using(minusOneIsNone) + Protocol("BITOP", "NOT" :: destinationKey.value :: key.value :: Nil).as[Num, NonNegInt] + final def bitpos(key: Key, bit: Bit): Protocol.Aux[Option[NonNegInt]] = Protocol("BITPOS", key :: bit :: HNil).using(minusOneIsNone) final def bitpos(key: Key, bit: Bit, start: Index): Protocol.Aux[Option[NonNegInt]] = Protocol("BITPOS", key :: bit :: start :: HNil).using(minusOneIsNone) - final def bitpos(key: Key, bit: Bit, start: Index, end: Index): Protocol.Aux[Option[NonNegInt]] = Protocol("BITPOS", key :: bit :: start :: end :: HNil).using(minusOneIsNone) - final def decr[A](key: Key)( - implicit ev: Integer ==> A - ): Protocol.Aux[A] = Protocol("DECR", key).as[Integer, A] + final def decr[A: Num ==> ?](key: Key): Protocol.Aux[A] = Protocol("DECR", key).as[Num, A] //TODO verify ok to limit DECRBY to only positive values, REDIS happily accepts 0 and negatives and x + (-decrement) - final def decrby[A](key: Key, decrement: PosLong)( - implicit ev: Integer ==> A - ): Protocol.Aux[A] = Protocol("DECRBY", key :: decrement :: HNil).as[Integer, A] + final def decrby[A: Num ==> ?](key: Key, decrement: PosLong): Protocol.Aux[A] = Protocol("DECRBY", key :: decrement :: HNil).as[Num, A] - final def get[A](key: Key)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[Option[A]] = - Protocol("GET", key).asC[NullBulkString :+: NonNullBulkString :+: CNil, Option[A]] + final def get[A: Bulk ==> ?](key: Key): Protocol.Aux[Option[A]] = Protocol("GET", key).opt[GenBulk].as[A] - final def getbit(key: Key, offset: PosLong): Protocol.Aux[Bit] = - Protocol("GETBIT", key :: offset :: HNil).as[Integer, Bit] + final def getbit(key: Key, offset: PosLong): Protocol.Aux[Bit] = Protocol("GETBIT", key :: offset :: HNil).as[Num, Bit] - final def getrange[A](key: Key, start: Index, end: Index)( - implicit ev: NonNullBulkString ==> A - ): Protocol.Aux[A] = Protocol("GETRANGE", key :: start :: end :: HNil).as[NonNullBulkString, A] + final def getrange[A: Bulk ==> ?](key: Key, start: Index, end: Index): Protocol.Aux[A] = + Protocol("GETRANGE", key :: start :: end :: HNil).as[Bulk, A] final def getset[A]: PartiallyAppliedGetSet[A] = new PartiallyAppliedGetSet[A](false) - final def incr[A](key: Key)( - implicit ev: Integer ==> A - ): Protocol.Aux[A] = Protocol("INCR", key).as[Integer, A] + final def incr[A: Num ==> ?](key: Key): Protocol.Aux[A] = Protocol("INCR", key).as[Num, A] //TODO verify ok to limit INCRBY to only positive values, REDIS happily accepts 0 and negatives - final def incrby[A](key: Key, increment: PosLong)( - implicit ev: Integer ==> A - ): Protocol.Aux[A] = Protocol("INCRBY", key :: increment :: HNil).as[Integer, A] + final def incrby[A: Num ==> ?](key: Key, increment: PosLong): Protocol.Aux[A] = Protocol("INCRBY", key :: increment :: HNil).as[Num, A] final def incrbyfloat(key: Key, increment: NonZeroDouble): Protocol.Aux[Double] = - Protocol("INCRBYFLOAT", key :: increment :: HNil).as[NonNullBulkString, Double] + Protocol("INCRBYFLOAT", key :: increment :: HNil).as[Bulk, Double] - final def mget[A](keys: OneOrMoreKeys)( - implicit ev: NonNilArray ==> A - ): Protocol.Aux[A] = Protocol("MGET", keys.value).as[NonNilArray, A] + final def mget[A: Arr ==> ?](keys: OneOrMoreKeys): Protocol.Aux[A] = Protocol("MGET", keys.value).as[Arr, A] - final def mset[L <: HList: RESPParamWrite, N <: Nat](l: L)( + final def mset[L <: HList: RESPParamWrite: LUBConstraint[?, (Key, _)], N <: Nat](l: L)( implicit ev0: Length.Aux[L, N], - ev1: N >= _1, - ev2: LUBConstraint[L, (Key, _)] - ): Protocol.Aux[OK] = Protocol("MSET", l).as[SimpleString, OK] - + ev1: N >= _1 + ): Protocol.Aux[OK] = Protocol("MSET", l).as[Str, OK] final def mset[P <: Product, L <: HList, N <: Nat](product: P)( implicit gen: LabelledGeneric.Aux[P, L], ev0: Length.Aux[L, N], ev1: N >= _1, ev2: LUBConstraint[L, FieldType[_, _]], ev3: RESPParamWrite[L] - ): Protocol.Aux[OK] = Protocol("MSET", gen.to(product)).as[SimpleString, OK] + ): Protocol.Aux[OK] = Protocol("MSET", gen.to(product)).as[Str, OK] - final def mset[A: Show](values: OneOrMore[(Key, A)]): Protocol.Aux[OK] = - Protocol("MSET", values.value).as[SimpleString, OK] + final def mset[A: Show](values: OneOrMore[(Key, A)]): Protocol.Aux[OK] = Protocol("MSET", values.value).as[Str, OK] - final def msetnx[L <: HList: RESPParamWrite, N <: Nat](l: L)( + final def msetnx[L <: HList: RESPParamWrite: LUBConstraint[?, (Key, _)], N <: Nat](l: L)( implicit ev0: Length.Aux[L, N], - ev1: N >= _1, - ev2: LUBConstraint[L, (Key, _)] - ): Protocol.Aux[Boolean] = Protocol("MSETNX", l).as[Integer, Boolean] + ev1: N >= _1 + ): Protocol.Aux[Boolean] = Protocol("MSETNX", l).as[Num, Boolean] final def msetnx[P <: Product, L <: HList, N <: Nat](product: P)( implicit gen: LabelledGeneric.Aux[P, L], @@ -184,41 +154,33 @@ trait StringP { ev1: N >= _1, ev2: LUBConstraint[L, FieldType[_, _]], ev3: RESPParamWrite[L] - ): Protocol.Aux[Boolean] = Protocol("MSETNX", gen.to(product)).as[Integer, Boolean] - + ): Protocol.Aux[Boolean] = Protocol("MSETNX", gen.to(product)).as[Num, Boolean] final def msetnx[A: Show](values: OneOrMore[(Key, A)]): Protocol.Aux[Boolean] = - Protocol("MSETNX", values.value).as[Integer, Boolean] + Protocol("MSETNX", values.value).as[Num, Boolean] final def psetex[A: Show](key: Key, milliseconds: PosLong, value: A): Protocol.Aux[OK] = - Protocol("PSETEX", key :: milliseconds :: value :: HNil).as[SimpleString, OK] - - final def set[A: Show](key: Key, value: A): Protocol.Aux[OK] = - Protocol("SET", key :: value :: HNil).as[SimpleString, OK] + Protocol("PSETEX", key :: milliseconds :: value :: HNil).as[Str, OK] + final def set[A: Show](key: Key, value: A): Protocol.Aux[OK] = Protocol("SET", key :: value :: HNil).as[Str, OK] final def set[A: Show](key: Key, value: A, expiry: Expiry): Protocol.Aux[OK] = - Protocol("SET", key :: value :: expiry.unit :: expiry.value :: HNil).as[SimpleString, OK] - + Protocol("SET", key :: value :: expiry.unit :: expiry.value :: HNil).as[Str, OK] final def set[A: Show](key: Key, value: A, flag: Flag): Protocol.Aux[Option[OK]] = - Protocol("SET", key :: value :: flag :: HNil).asC[NullBulkString :+: SimpleString :+: CNil, Option[OK]] - + Protocol("SET", key :: value :: flag :: HNil).asC[Str :+: NullBulk :+: CNil, Option[OK]] final def set[A: Show](key: Key, value: A, expiry: Expiry, flag: Flag): Protocol.Aux[Option[OK]] = - Protocol("SET", key :: value :: flag :: expiry.unit :: expiry.value :: HNil) - .asC[NullBulkString :+: SimpleString :+: CNil, Option[OK]] + Protocol("SET", key :: value :: flag :: expiry.unit :: expiry.value :: HNil).asC[Str :+: NullBulk :+: CNil, Option[OK]] final def setbit(key: Key, offset: StringLength, bit: Bit): Protocol.Aux[Bit] = - Protocol("SETBIT", key :: offset :: bit :: HNil).as[Integer, Bit] + Protocol("SETBIT", key :: offset :: bit :: HNil).as[Num, Bit] final def setex[A: Show](key: Key, value: A, seconds: PosLong): Protocol.Aux[OK] = - Protocol("SETEX", key :: seconds :: value :: HNil).as[SimpleString, OK] + Protocol("SETEX", key :: seconds :: value :: HNil).as[Str, OK] - final def setnx[A: Show](key: Key, value: A): Protocol.Aux[Boolean] = - Protocol("SETNX", key :: value :: HNil).as[Integer, Boolean] + final def setnx[A: Show](key: Key, value: A): Protocol.Aux[Boolean] = Protocol("SETNX", key :: value :: HNil).as[Num, Boolean] final def setrange[A: Show](key: Key, offset: RangeOffset, value: A): Protocol.Aux[NonNegInt] = - Protocol("SETRANGE", key :: offset :: value :: HNil).as[Integer, NonNegInt] + Protocol("SETRANGE", key :: offset :: value :: HNil).as[Num, NonNegInt] - final def strlen(key: Key): Protocol.Aux[NonNegInt] = - Protocol("STRLEN", key :: HNil).as[Integer, NonNegInt] + final def strlen(key: Key): Protocol.Aux[NonNegInt] = Protocol("STRLEN", key :: HNil).as[Num, NonNegInt] } -trait AllStringP extends StringP with StringPExtra +trait StringP extends StringBaseP with StringExtP diff --git a/core/src/main/scala/laserdisc/protocol/TransactionP.scala b/core/src/main/scala/laserdisc/protocol/TransactionP.scala new file mode 100644 index 00000000..4a102f9b --- /dev/null +++ b/core/src/main/scala/laserdisc/protocol/TransactionP.scala @@ -0,0 +1,10 @@ +package laserdisc +package protocol + +trait TransactionP { + //TODO: discard? exec/multi? + + final val unwatch: Protocol.Aux[OK] = Protocol("UNWATCH", Nil).as[Str, OK] + + final def watch(keys: OneOrMoreKeys): Protocol.Aux[OK] = Protocol("WATCH", keys.value).as[Str, OK] +} diff --git a/core/src/main/scala/laserdisc/syntax.scala b/core/src/main/scala/laserdisc/syntax.scala index f39ecd18..b9a91389 100644 --- a/core/src/main/scala/laserdisc/syntax.scala +++ b/core/src/main/scala/laserdisc/syntax.scala @@ -1,26 +1,32 @@ package laserdisc +object cluster extends protocol.ClusterP object connection extends protocol.ConnectionP -object hashmaps extends protocol.AllHashP -object hyperloglog extends protocol.AllHyperLogLogP -object keys extends protocol.AllKeyP -object lists extends protocol.AllListP { object blocking extends protocol.AllBListP } +object geo extends protocol.GeoP +object hashmaps extends protocol.HashP +object hyperloglog extends protocol.HyperLogLogP +object keys extends protocol.KeyP +object lists extends protocol.ListP { object blocking extends protocol.BListP } object publish extends protocol.PublishP object server extends protocol.ServerP -object sets extends protocol.AllSetP -object sortedsets extends protocol.AllSortedSetP -object strings extends protocol.AllStringP +object sets extends protocol.SetP +object sortedsets extends protocol.SortedSetP +object strings extends protocol.StringP +object transaction extends protocol.TransactionP object all - extends protocol.ConnectionP - with protocol.AllHashP - with protocol.AllHyperLogLogP - with protocol.AllKeyP - with protocol.AllListP + extends protocol.ClusterP + with protocol.ConnectionP + with protocol.GeoP + with protocol.HashP + with protocol.HyperLogLogP + with protocol.KeyP + with protocol.ListP with protocol.PublishP with protocol.ServerP - with protocol.AllSetP - with protocol.AllSortedSetP - with protocol.AllStringP { - final object blocking extends protocol.AllBListP + with protocol.SetP + with protocol.SortedSetP + with protocol.StringP + with protocol.TransactionP { + final object blocking extends protocol.BListP } diff --git a/core/src/main/scala/laserdisc/types.scala b/core/src/main/scala/laserdisc/types.scala index 130a7182..56d1e893 100644 --- a/core/src/main/scala/laserdisc/types.scala +++ b/core/src/main/scala/laserdisc/types.scala @@ -1,8 +1,25 @@ package laserdisc -final case class NaN() +final case class ControlChar() +object ControlChar { + import eu.timepit.refined.api.Validate + + implicit final val controlCharValidate: Validate.Plain[Char, ControlChar] = + Validate.fromPredicate(_.isControl, t => s"isControl('$t')", ControlChar()) +} final case class KV[A](key: Key, value: A) final case class ScanKV(cursor: NonNegLong, maybeValues: Option[Seq[KV[String]]]) final case class Scan[A](cursor: NonNegLong, values: Option[Seq[A]]) final case class Time(timestamp: NonNegLong, elapsedMicroseconds: NonNegLong) + +sealed trait Direction +object Direction { + final object asc extends Direction + final object desc extends Direction + + implicit val directionShow: Show[Direction] = Show.instance { + case `asc` => "ASC" + case `desc` => "DESC" + } +} diff --git a/core/src/test/boilerplate/BListExtPSpec.scala.template b/core/src/test/boilerplate/BListExtPSpec.scala.template new file mode 100644 index 00000000..8fd42ea2 --- /dev/null +++ b/core/src/test/boilerplate/BListExtPSpec.scala.template @@ -0,0 +1,103 @@ +package laserdisc +package protocol + +abstract class BListExtPSpec extends BaseSpec with BListP { + + "The Blocking List extended protocol" when { + + "using blpop" should { + + "fail to compile" when { + "given key but missing read instance" in { + """blpop[Bar](Key("a"))""" shouldNot compile + } + "given key and timeout but missing read instance" in { + """blpop[Bar](Key("a"), PosInt(1))""" shouldNot compile + } + } + + "roundtrip successfully" when { + //BLPOP with no timeout specified + [1..5#"given [#key1#]" in forAll([#"key1"#], "returned value") { ([#k1: Key#], i: Int) => + val protocol = blpop[Int]([#k1#]) + + protocol.encode shouldBe Arr(Bulk("BLPOP"), [#Bulk(k1)#], Bulk(##0)) + protocol.decode(Arr(Bulk(k##1), Bulk(i))).right.value.value shouldBe KV(k##1, i) + }# + ] + //BLPOP with no timeout specified and specific read instance + [1..5#"given [#key1#] and specific read instance" in forAll([#"key1"#], "returned value") { ([#k1: Key#], i: Int) => + val protocol = blpop[Foo]([#k1#]) + + protocol.encode shouldBe Arr(Bulk("BLPOP"), [#Bulk(k1)#], Bulk(##0)) + protocol.decode(Arr(Bulk(k##1), Bulk(i))).right.value.value shouldBe KV(k##1, Foo(i)) + }# + ] + //BLPOP with timeout specified + [1..4#"given [#key1#] and positive timeout" in forAll([#"key1"#], "timeout", "returned value") { ([#k1: Key#], pi: PosInt, i: Int) => + val protocol = blpop[Int]([#k1#], pi) + + protocol.encode shouldBe Arr(Bulk("BLPOP"), [#Bulk(k1)#], Bulk(pi)) + protocol.decode(Arr(Bulk(k##1), Bulk(i))).right.value.value shouldBe KV(k##1, i) + }# + ] + //BLPOP with timeout specified and specific read instance + [1..4#"given [#key1#], positive timeout and specific read instance" in forAll([#"key1"#], "timeout", "returned value") { ([#k1: Key#], pi: PosInt, i: Int) => + val protocol = blpop[Foo]([#k1#], pi) + + protocol.encode shouldBe Arr(Bulk("BLPOP"), [#Bulk(k1)#], Bulk(pi)) + protocol.decode(Arr(Bulk(k##1), Bulk(i))).right.value.value shouldBe KV(k##1, Foo(i)) + }# + ] + } + + } + + "using brpop" should { + + "fail to compile" when { + "given key but missing read instance" in { + """brpop[Bar](Key("a"))""" shouldNot compile + } + "given key and timeout but missing read instance" in { + """brpop[Bar](Key("a"), PosInt(1))""" shouldNot compile + } + } + + "roundtrip successfully" when { + //BRPOP with no timeout specified + [1..5#"given [#key1#]" in forAll([#"key1"#], "returned value") { ([#k1: Key#], i: Int) => + val protocol = brpop[Int]([#k1#]) + + protocol.encode shouldBe Arr(Bulk("BRPOP"), [#Bulk(k1)#], Bulk(##0)) + protocol.decode(Arr(Bulk(k##1), Bulk(i))).right.value.value shouldBe KV(k##1, i) + }# + ] + //BRPOP with no timeout specified and specific read instance + [1..5#"given [#key1#] and specific read instance" in forAll([#"key1"#], "returned value") { ([#k1: Key#], i: Int) => + val protocol = brpop[Foo]([#k1#]) + + protocol.encode shouldBe Arr(Bulk("BRPOP"), [#Bulk(k1)#], Bulk(##0)) + protocol.decode(Arr(Bulk(k##1), Bulk(i))).right.value.value shouldBe KV(k##1, Foo(i)) + }# + ] + //BRPOP with timeout specified + [1..4#"given [#key1#] and positive timeout" in forAll([#"key1"#], "timeout", "returned value") { ([#k1: Key#], pi: PosInt, i: Int) => + val protocol = brpop[Int]([#k1#], pi) + + protocol.encode shouldBe Arr(Bulk("BRPOP"), [#Bulk(k1)#], Bulk(pi)) + protocol.decode(Arr(Bulk(k##1), Bulk(i))).right.value.value shouldBe KV(k##1, i) + }# + ] + //BRPOP with timeout specified and specific read instance + [1..4#"given [#key1#], positive timeout and specific read instance" in forAll([#"key1"#], "timeout", "returned value") { ([#k1: Key#], pi: PosInt, i: Int) => + val protocol = brpop[Foo]([#k1#], pi) + + protocol.encode shouldBe Arr(Bulk("BRPOP"), [#Bulk(k1)#], Bulk(pi)) + protocol.decode(Arr(Bulk(k##1), Bulk(i))).right.value.value shouldBe KV(k##1, Foo(i)) + }# + ] + } + } + } +} diff --git a/core/src/test/boilerplate/GeoExtPSpec.scala.template b/core/src/test/boilerplate/GeoExtPSpec.scala.template new file mode 100644 index 00000000..5b0a0c89 --- /dev/null +++ b/core/src/test/boilerplate/GeoExtPSpec.scala.template @@ -0,0 +1,85 @@ +package laserdisc +package protocol + +abstract class GeoExtPSpec extends BaseSpec with GeoP { + import geotypes._ + import org.scalacheck.{Arbitrary, Gen} + import org.scalacheck.Arbitrary.arbitrary + + protected implicit final val geoHashShow: Show[GeoHash] = Show.unsafeFromToString + + protected implicit final val geoCoordinatesArb: Arbitrary[GeoCoordinates] = Arbitrary { + for { + lat <- arbitrary[Latitude] + long <- arbitrary[Longitude] + } yield GeoCoordinates(lat, long) + } + protected implicit final val geoPositionArb: Arbitrary[GeoPosition] = Arbitrary { + for { + m <- arbitrary[Key] + lat <- arbitrary[Latitude] + long <- arbitrary[Longitude] + } yield GeoPosition(m, lat, long) + } + protected implicit final val geoUnitArb: Arbitrary[GeoUnit] = Arbitrary { + Gen.oneOf(GeoUnit.meters, GeoUnit.kilometers, GeoUnit.miles, GeoUnit.feet) + } + + protected final val geoPositionToBulkList: GeoPosition => List[Bulk] = { + case GeoPosition(m, lat, long) => Bulk(long) :: Bulk(lat) :: Bulk(m) :: Nil + } + protected final val nonNegDoubleOptionToBulk: Option[NonNegDouble] => GenBulk = _.fold(NullBulk: GenBulk)(Bulk(_)) + protected final val oneOrMoreGeoCoordinatesOptionToArr: OneOrMore[Option[GeoCoordinates]] => GenArr = _.value.foldLeft(NilArr: GenArr) { + case (NilArr, Some(GeoCoordinates(lat, long))) => Arr(Arr(Bulk(long), Bulk(lat))) + case (NilArr, None) => Arr(NilArr) + case (Arr(e), Some(GeoCoordinates(lat, long))) => Arr(e :+ Arr(Bulk(long), Bulk(lat))) + case (Arr(e), None) => Arr(e :+ NilArr) + } + protected final val oneOrMoreGeoHashOptionToArr: OneOrMore[Option[GeoHash]] => GenArr = _.value.foldLeft(NilArr: GenArr) { + case (NilArr, Some(gh)) => Arr(Bulk(gh)) + case (NilArr, None) => Arr(NullBulk) + case (Arr(e), Some(gh)) => Arr(e :+ Bulk(gh)) + case (Arr(e), None) => Arr(e :+ NullBulk) + } + + "The Geo extended protocol" when { + + "using geoadd" should { + "roundtrip successfully" when { + [1..4#"given key and [#position1#]" in forAll("key", [#"position1"#], "added") { (k: Key, [#p1: GeoPosition#], nni: NonNegInt) => + val protocol = geoadd(k, [#p1#]) + + protocol.encode shouldBe Arr(Bulk("GEOADD") :: Bulk(k) :: List([#p1#]).flatMap(geoPositionToBulkList)) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + }# + ] + } + } + + "using geohash" should { + + "roundtrip successfully" when { + [1..4#"given key and [#member1#]" in forAll("key", [#"member1"#], "geo hashes") { (k: Key, [#m1: Key#], oghs: OneOrMore[Option[GeoHash]]) => + val protocol = geohash(k, [#m1#]) + + protocol.encode shouldBe Arr(Bulk("GEOHASH") :: Bulk(k) :: List([#Bulk(m1)#])) + protocol.decode(oneOrMoreGeoHashOptionToArr(oghs)).right.value shouldBe oghs.value + }# + ] + } + } + + "using geopos" should { + + "roundtrip successfully" when { + [1..4#"given key and [#member1#]" in forAll("key", [#"member1"#], "coordinates") { (k: Key, [#m1: Key#], ocs: OneOrMore[Option[GeoCoordinates]]) => + val protocol = geopos(k, [#m1#]) + + protocol.encode shouldBe Arr(Bulk("GEOPOS") :: Bulk(k) :: List([#Bulk(m1)#])) + protocol.decode(oneOrMoreGeoCoordinatesOptionToArr(ocs)).right.value shouldBe ocs.value + }# + ] + } + } + } +} diff --git a/core/src/test/boilerplate/HashExtPSpec.scala.template b/core/src/test/boilerplate/HashExtPSpec.scala.template new file mode 100644 index 00000000..aeb6796e --- /dev/null +++ b/core/src/test/boilerplate/HashExtPSpec.scala.template @@ -0,0 +1,51 @@ +package laserdisc +package protocol + +abstract class HashExtPSpec extends BaseSpec with HashP { + + "The Hash extended protocol" when { + + "using hdel" should { + + "roundtrip successfully" when { + [1..4#"given key and [#field1#]" in forAll("key", [#"field1"#], "deleted") { (k: Key, [#f1: Key#], nni: NonNegInt) => + val protocol = hdel(k, [#f1#]) + + protocol.encode shouldBe Arr(Bulk("HDEL"), Bulk(k), [#Bulk(f1)#]) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + }# + ] + } + } + + "using hmget" should { + + "roundtrip successfully" when { + [1..5#"given key and [#field1#]" in forAll("key", [#"field1"#]) { (k: Key, [#f1: Key#]) => + forAll([#"returned value1"#]) { ([#v1: Int#]) => + val protocol = hmget[[#Int#]](k, [#f1#]) + + protocol.encode shouldBe Arr(Bulk("HMGET"), Bulk(k), [#Bulk(f1)#]) + protocol.decode(Arr([#Bulk(v1)#])).right.value shouldBe (([#v1#])) + } + }# + ] + } + } + + "using hmset" should { + + "roundtrip successfully" when { + [1..5#"given key and [#field1#]" in forAll("key", [#"field1"#]) { (k: Key, [#f1: Key#]) => + forAll([#"value1"#]) { ([#v1: Int#]) => + val protocol = hmset(k, [#f1, v1#]) + + protocol.encode shouldBe Arr(Bulk("HMSET"), Bulk(k), [#Bulk(f1), Bulk(v1)#]) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + }# + ] + } + } + } +} diff --git a/core/src/test/boilerplate/HyperLogLogExtPSpec.scala.template b/core/src/test/boilerplate/HyperLogLogExtPSpec.scala.template new file mode 100644 index 00000000..e59f5542 --- /dev/null +++ b/core/src/test/boilerplate/HyperLogLogExtPSpec.scala.template @@ -0,0 +1,47 @@ +package laserdisc +package protocol + +abstract class HyperLogLogExtPSpec extends BaseSpec with HyperLogLogP { + + "The HyperLogLog extended protocol" when { + + "using pfadd" should { + + "roundtrip successfully" when { + [1..4#"given key and [#element1#]" in forAll("key", [#"element1"#], "return value") { (k: Key, [#e1: Key#], b: Boolean) => + val protocol = pfadd(k, [#e1#]) + + protocol.encode shouldBe Arr(Bulk("PFADD"), Bulk(k), [#Bulk(e1)#]) + protocol.decode(boolToNum(b)).right.value shouldBe b + }# + ] + } + } + + "using pfcount" should { + + "roundtrip successfully" when { + [1..5#"given [#key1#]" in forAll([#"key1"#], "return value") { ([#k1: Key#], nni: NonNegInt) => + val protocol = pfcount([#k1#]) + + protocol.encode shouldBe Arr(Bulk("PFCOUNT"), [#Bulk(k1)#]) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + }# + ] + } + } + + "using pfmerge" should { + + "roundtrip successfully" when { + [2..4#"given [#sourcekey1#] and destinationkey" in forAll([#"sourcekey1"#], "destinationkey") { ([#sk1: Key#], dk: Key) => + val protocol = pfmerge([#sk1#], dk) + + protocol.encode shouldBe Arr(Bulk("PFMERGE"), Bulk(dk), [#Bulk(sk1)#]) + protocol.decode(Str(OK.value)).right.value shouldBe OK + }# + ] + } + } + } +} diff --git a/core/src/test/boilerplate/KeyExtPSpec.scala.template b/core/src/test/boilerplate/KeyExtPSpec.scala.template new file mode 100644 index 00000000..4f066e8b --- /dev/null +++ b/core/src/test/boilerplate/KeyExtPSpec.scala.template @@ -0,0 +1,105 @@ +package laserdisc +package protocol + +abstract class KeyExtPSpec extends BaseSpec with KeyP { + import keytypes._ + import org.scalacheck.{Arbitrary, Gen} + + protected implicit final val keyMigrateModeArb: Arbitrary[KeyMigrateMode] = Arbitrary { + Gen.oneOf(KeyMigrateMode.both, KeyMigrateMode.copy, KeyMigrateMode.replace) + } + protected implicit final val nokeyOrOkArb: Arbitrary[NOKEY | OK] = Arbitrary( + Gen.oneOf(NOKEY, OK).map { + case NOKEY => Left(NOKEY) + case OK => Right(OK) + } + ) + + protected final val noKeyOrOkToStr: (NOKEY | OK) => Str = { + case Left(_) => Str(NOKEY.value) + case Right(_) => Str(OK.value) + } + + "The Key extended protocol" when { + + "using del" should { + + "roundtrip successfully" when { + [1..5#"given [#key1#]" in forAll([#"key1"#], "deleted") { ([#k1: Key#], nni: NonNegInt) => + val protocol = del([#k1#]) + + protocol.encode shouldBe Arr(Bulk("DEL"), [#Bulk(k1)#]) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + }# + ] + } + } + + "using exists" should { + + "roundtrip successfully" when { + [1..5#"given [#key1#]" in forAll([#"key1"#], "exists") { ([#k1: Key#], opi: Option[PosInt]) => + val protocol = exists([#k1#]) + + protocol.encode shouldBe Arr(Bulk("EXISTS"), [#Bulk(k1)#]) + protocol.decode(Num(opi.fold(##0L)(_.value.toLong))).right.value shouldBe opi + }# + ] + } + } + + "using migrate" should { + + "roundtrip successfully" when { + [2..6#"given [#key1#], host, port, db index and timeout" in forAll([#"key1"#]) { ([#k1: Key#]) => + forAll("host", "port", "db index", "timeout", "response") { (h: Host, p: Port, dbi: DbIndex, nni: NonNegInt, nkOrOk: NOKEY | OK) => + val protocol = migrate([#k1#], h, p, dbi, nni) + + protocol.encode shouldBe Arr( + Bulk("MIGRATE") :: Bulk(h) :: Bulk(p) :: Bulk("") :: Bulk(dbi) :: Bulk(nni) :: Bulk("KEYS") :: [#Bulk(k1)# ::] :: Nil + ) + protocol.decode(noKeyOrOkToStr(nkOrOk)).right.value shouldBe nkOrOk + } + }# + ] + [2..6#"given [#key1#], host, port, db index, timeout and migrate mode" in forAll([#"key1"#]) { ([#k1: Key#]) => + forAll("host", "port", "db index", "timeout", "migrate mode", "response") { (h: Host, p: Port, dbi: DbIndex, nni: NonNegInt, mm: KeyMigrateMode, nkOrOk: NOKEY | OK) => + val protocol = migrate([#k1#], h, p, dbi, nni, mm) + + protocol.encode shouldBe Arr( + Bulk("MIGRATE") :: Bulk(h) :: Bulk(p) :: Bulk("") :: Bulk(dbi) :: Bulk(nni) :: mm.params.map(Bulk(_)) ::: (Bulk("KEYS") :: [#Bulk(k1)# ::] :: Nil) + ) + protocol.decode(noKeyOrOkToStr(nkOrOk)).right.value shouldBe nkOrOk + } + }# + ] + } + } + + "using touch" should { + + "roundtrip successfully" when { + [1..5#"given [#key1#]" in forAll([#"key1"#], "touched") { ([#k1: Key#], nni: NonNegInt) => + val protocol = touch([#k1#]) + + protocol.encode shouldBe Arr(Bulk("TOUCH"), [#Bulk(k1)#]) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + }# + ] + } + } + + "using unlink" should { + + "roundtrip successfully" when { + [1..5#"given [#key1#]" in forAll([#"key1"#], "unlinked") { ([#k1: Key#], nni: NonNegInt) => + val protocol = unlink([#k1#]) + + protocol.encode shouldBe Arr(Bulk("UNLINK"), [#Bulk(k1)#]) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + }# + ] + } + } + } +} diff --git a/core/src/test/boilerplate/ListExtPSpec.scala.template b/core/src/test/boilerplate/ListExtPSpec.scala.template new file mode 100644 index 00000000..8b402b7e --- /dev/null +++ b/core/src/test/boilerplate/ListExtPSpec.scala.template @@ -0,0 +1,34 @@ +package laserdisc +package protocol + +abstract class ListExtPSpec extends BaseSpec with ListP { + + "The List extended protocol" when { + + "using lpush" should { + + "roundtrip successfully" when { + [1..4#"given key and [#value1#]" in forAll("key", [#"value1"#], "pushed") { (k: Key, [#v1: Int#], pi: PosInt) => + val protocol = lpush(k, [#v1#]) + + protocol.encode shouldBe Arr(Bulk("LPUSH"), Bulk(k), [#Bulk(v1)#]) + protocol.decode(Num(pi.value.toLong)).right.value shouldBe pi + }# + ] + } + } + + "using rpush" should { + + "roundtrip successfully" when { + [1..4#"given key and [#value1#]" in forAll("key", [#"value1"#], "pushed") { (k: Key, [#v1: Int#], pi: PosInt) => + val protocol = rpush(k, [#v1#]) + + protocol.encode shouldBe Arr(Bulk("RPUSH"), Bulk(k), [#Bulk(v1)#]) + protocol.decode(Num(pi.value.toLong)).right.value shouldBe pi + }# + ] + } + } + } +} diff --git a/core/src/test/scala-2.11/laserdisc/HashPSpec.scala b/core/src/test/scala-2.11/laserdisc/HashPSpec.scala deleted file mode 100644 index 626750f7..00000000 --- a/core/src/test/scala-2.11/laserdisc/HashPSpec.scala +++ /dev/null @@ -1,253 +0,0 @@ -package laserdisc - -import org.scalatest.{Matchers, WordSpecLike} - -final class HashPSpec extends WordSpecLike with Matchers { - - "A HashP" when { - - "using hdel" should { - - "fail to compile" when { - "given empty key" in { - """import auto._ - |hashmaps.hdel("", "f")""".stripMargin shouldNot compile - } - "given empty field" in { - """import auto._ - |hashmaps.hdel("a", "")""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given non empty key and one non empty field" in { - """import auto._ - |hashmaps.hdel("a", "f1")""".stripMargin should compile - } - "given non empty key and two non empty fields" in { - """import auto._ - |hashmaps.hdel("a", "f1", "f2")""".stripMargin should compile - } - } - - } - - "using hexists" should { - - "fail to compile" when { - "given empty key" in { - """import auto._ - |hashmaps.hexists("", "f")""".stripMargin shouldNot compile - } - "given empty field" in { - """import auto._ - |hashmaps.hexists("a", "")""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given non empty key and non empty field" in { - """import auto._ - |hashmaps.hexists("a", "f")""".stripMargin should compile - } - } - - } - - "using hget" should { - - "fail to compile" when { - "given empty key" in { - """import auto._ - |hashmaps.hget("", "f")""".stripMargin shouldNot compile - } - "given empty field" in { - """import auto._ - |hashmaps.hget("a", "")""".stripMargin shouldNot compile - } - "missing read instance" in { - """import auto._ - |case class Foo(x: Int) - |hashmaps.hget[Foo]("a", "f")""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given non empty key and non empty field" in { - """import auto._ - |hashmaps.hget("a", "f1")""".stripMargin should compile - } - "given specific read instance" in { - """import auto._ - |case class Foo(x: String) - |implicit val fooRead: Read[protocol.NonNullBulkString, Foo] = Read.instancePF { - | case protocol.NonNullBulkString(x) => Foo(x) - |} - |hashmaps.hget[Foo]("a", "f")""".stripMargin should compile - } - } - - } - - "using hgetall" should { - - "fail to compile" when { - "given empty key" in { - """import auto._ - |hashmaps.hgetall("", "f")""".stripMargin shouldNot compile - } - "missing read instance" in { - """import auto._ - |hashmaps.hgetall[Map[String, Int]]("a")""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given non empty key" in { - """import auto._ - |hashmaps.hgetall("a")""".stripMargin should compile - } - "using OOB read instance" in { - """import auto._ - |hashmaps.hgetall[Map[Key, String]]("a")""".stripMargin should compile - } - "deriving HList read instance" in { - """import auto._, shapeless._ - |hashmaps.hgetall[Key :: Int :: HNil]("a")""".stripMargin should compile - } - "deriving Product read instance" in { - """import auto._ - |case class Foo(a: String) - |hashmaps.hgetall[Foo]("a")""".stripMargin should compile - } - } - - } - - "using hincrby" should { - - "fail to compile" when { - "given empty key" in { - """import auto._ - |hashmaps.hincrby("", "f", 1L)""".stripMargin shouldNot compile - } - "given empty field" in { - """import auto._ - |hashmaps.hincrby("a", "", 1L)""".stripMargin shouldNot compile - } - "given zero increment" in { - """import auto._ - |hashmaps.hincrby("a", "f", 0L)""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given non empty key, non empty field and positive increment" in { - """import auto._ - |hashmaps.hincrby("a", "f", 1L)""".stripMargin should compile - } - "given non empty key, non empty field and negative increment" in { - """import auto._ - |hashmaps.hincrby("a", "f", -1L)""".stripMargin should compile - } - } - - } - - "using hincrbyfloat" should { - - "fail to compile" when { - "given empty key" in { - """import auto._ - |hashmaps.hincrbyfloat("", "f", 1d)""".stripMargin shouldNot compile - } - "given empty field" in { - """import auto._ - |hashmaps.hincrbyfloat("a", "", 1d)""".stripMargin shouldNot compile - } - "given zero increment" in { - """import auto._ - |hashmaps.hincrbyfloat("a", "f", 0d)""".stripMargin shouldNot compile - } - "given NaN increment" in { - """import auto._ - |hashmaps.hincrbyfloat("a", "f", java.lang.Double.NaN)""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given non empty key, non empty field and positive increment" in { - """import auto._ - |hashmaps.hincrbyfloat("a", "f", NonZeroDouble.unsafeFrom(1d))""".stripMargin should compile - } - "given non empty key, non empty field and negative increment" in { - """import auto._ - |hashmaps.hincrbyfloat("a", "f", NonZeroDouble.unsafeFrom(-1d))""".stripMargin should compile - } - } - - } - - "using hkeys" should { - - "fail to compile" when { - "given empty key" in { - """import auto._ - |hashmaps.hkeys("")""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given non empty key" in { - """import auto._ - |hashmaps.hkeys("a")""".stripMargin should compile - } - } - - } - - "using hlen" should { - - "fail to compile" when { - "given empty key" in { - """import auto._ - |hashmaps.hlen("")""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given non empty key" in { - """import auto._ - |hashmaps.hlen("a")""".stripMargin should compile - } - } - - } - - "using hmget" should { - - "fail to compile" when { - "given empty key" in { - """import auto._ - |hashmaps.hmget("", "f")""".stripMargin shouldNot compile - } - "given empty field" in { - """import auto._ - |hashmaps.hmget("a", "")""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given non empty key and one non empty field" in { - """import auto._ - |hashmaps.hmget[Int]("a", "f1")""".stripMargin should compile - } - "given non empty key and two non empty fields" in { - """import auto._ - |hashmaps.hmget[Int, Int]("a", "f1", "f2")""".stripMargin should compile - } - } - - } - } -} diff --git a/core/src/test/scala-2.12/laserdisc/HashPSpec.scala b/core/src/test/scala-2.12/laserdisc/HashPSpec.scala deleted file mode 100644 index da2b2e4a..00000000 --- a/core/src/test/scala-2.12/laserdisc/HashPSpec.scala +++ /dev/null @@ -1,253 +0,0 @@ -package laserdisc - -import org.scalatest.{Matchers, WordSpecLike} - -final class HashPSpec extends WordSpecLike with Matchers { - - "A HashP" when { - - "using hdel" should { - - "fail to compile" when { - "given empty key" in { - """import auto._ - |hashmaps.hdel("", "f")""".stripMargin shouldNot compile - } - "given empty field" in { - """import auto._ - |hashmaps.hdel("a", "")""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given non empty key and one non empty field" in { - """import auto._ - |hashmaps.hdel("a", "f1")""".stripMargin should compile - } - "given non empty key and two non empty fields" in { - """import auto._ - |hashmaps.hdel("a", "f1", "f2")""".stripMargin should compile - } - } - - } - - "using hexists" should { - - "fail to compile" when { - "given empty key" in { - """import auto._ - |hashmaps.hexists("", "f")""".stripMargin shouldNot compile - } - "given empty field" in { - """import auto._ - |hashmaps.hexists("a", "")""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given non empty key and non empty field" in { - """import auto._ - |hashmaps.hexists("a", "f")""".stripMargin should compile - } - } - - } - - "using hget" should { - - "fail to compile" when { - "given empty key" in { - """import auto._ - |hashmaps.hget("", "f")""".stripMargin shouldNot compile - } - "given empty field" in { - """import auto._ - |hashmaps.hget("a", "")""".stripMargin shouldNot compile - } - "missing read instance" in { - """import auto._ - |case class Foo(x: Int) - |hashmaps.hget[Foo]("a", "f")""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given non empty key and non empty field" in { - """import auto._ - |hashmaps.hget("a", "f1")""".stripMargin should compile - } - "given specific read instance" in { - """import auto._ - |case class Foo(x: String) - |implicit val fooRead: Read[protocol.NonNullBulkString, Foo] = Read.instancePF { - | case protocol.NonNullBulkString(x) => Foo(x) - |} - |hashmaps.hget[Foo]("a", "f")""".stripMargin should compile - } - } - - } - - "using hgetall" should { - - "fail to compile" when { - "given empty key" in { - """import auto._ - |hashmaps.hgetall("", "f")""".stripMargin shouldNot compile - } - "missing read instance" in { - """import auto._ - |hashmaps.hgetall[Map[String, Int]]("a")""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given non empty key" in { - """import auto._ - |hashmaps.hgetall("a")""".stripMargin should compile - } - "using OOB read instance" in { - """import auto._ - |hashmaps.hgetall[Map[Key, String]]("a")""".stripMargin should compile - } - "deriving HList read instance" in { - """import auto._, shapeless._ - |hashmaps.hgetall[Key :: Int :: HNil]("a")""".stripMargin should compile - } - "deriving Product read instance" in { - """import auto._ - |case class Foo(a: String) - |hashmaps.hgetall[Foo]("a")""".stripMargin should compile - } - } - - } - - "using hincrby" should { - - "fail to compile" when { - "given empty key" in { - """import auto._ - |hashmaps.hincrby("", "f", 1L)""".stripMargin shouldNot compile - } - "given empty field" in { - """import auto._ - |hashmaps.hincrby("a", "", 1L)""".stripMargin shouldNot compile - } - "given zero increment" in { - """import auto._ - |hashmaps.hincrby("a", "f", 0L)""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given non empty key, non empty field and positive increment" in { - """import auto._ - |hashmaps.hincrby("a", "f", 1L)""".stripMargin should compile - } - "given non empty key, non empty field and negative increment" in { - """import auto._ - |hashmaps.hincrby("a", "f", -1L)""".stripMargin should compile - } - } - - } - - "using hincrbyfloat" should { - - "fail to compile" when { - "given empty key" in { - """import auto._ - |hashmaps.hincrbyfloat("", "f", 1d)""".stripMargin shouldNot compile - } - "given empty field" in { - """import auto._ - |hashmaps.hincrbyfloat("a", "", 1d)""".stripMargin shouldNot compile - } - "given zero increment" in { - """import auto._ - |hashmaps.hincrbyfloat("a", "f", 0d)""".stripMargin shouldNot compile - } - "given NaN increment" in { - """import auto._ - |hashmaps.hincrbyfloat("a", "f", java.lang.Double.NaN)""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given non empty key, non empty field and positive increment" in { - """import auto._ - |hashmaps.hincrbyfloat("a", "f", 1d)""".stripMargin should compile - } - "given non empty key, non empty field and negative increment" in { - """import auto._ - |hashmaps.hincrbyfloat("a", "f", -1d)""".stripMargin should compile - } - } - - } - - "using hkeys" should { - - "fail to compile" when { - "given empty key" in { - """import auto._ - |hashmaps.hkeys("")""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given non empty key" in { - """import auto._ - |hashmaps.hkeys("a")""".stripMargin should compile - } - } - - } - - "using hlen" should { - - "fail to compile" when { - "given empty key" in { - """import auto._ - |hashmaps.hlen("")""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given non empty key" in { - """import auto._ - |hashmaps.hlen("a")""".stripMargin should compile - } - } - - } - - "using hmget" should { - - "fail to compile" when { - "given empty key" in { - """import auto._ - |hashmaps.hmget("", "f")""".stripMargin shouldNot compile - } - "given empty field" in { - """import auto._ - |hashmaps.hmget("a", "")""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given non empty key and one non empty field" in { - """import auto._ - |hashmaps.hmget[Int]("a", "f1")""".stripMargin should compile - } - "given non empty key and two non empty fields" in { - """import auto._ - |hashmaps.hmget[Int, Int]("a", "f1", "f2")""".stripMargin should compile - } - } - - } - } -} diff --git a/core/src/test/scala/laserdisc/BListPSpec.scala b/core/src/test/scala/laserdisc/BListPSpec.scala deleted file mode 100644 index 242a7ad8..00000000 --- a/core/src/test/scala/laserdisc/BListPSpec.scala +++ /dev/null @@ -1,145 +0,0 @@ -package laserdisc - -import org.scalatest.{Matchers, WordSpecLike} - -final class BListPSpec extends WordSpecLike with Matchers { - - "A BListP" when { - - "using blpop" should { - - "fail to compile" when { - "given empty key" in { - """import auto._ - |lists.blocking.blpop("")""".stripMargin shouldNot compile - } - "given negative timeout" in { - """import auto._ - |lists.blocking.blpop("a", -1)""".stripMargin shouldNot compile - } - "given zero timeout" in { - """import auto._ - |lists.blocking.blpop("a", 0)""".stripMargin shouldNot compile - } - "missing read instance" in { - """import auto._ - |case class Foo(x: Int) - |lists.blocking.blpop[Foo]("a", 1)""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given one non empty key" in { - """import auto._ - |lists.blocking.blpop("a")""".stripMargin should compile - } - "given one non empty key and positive timeout" in { - """import auto._ - |lists.blocking.blpop("a", 1)""".stripMargin should compile - } - "given two non empty keys" in { - """import auto._ - |lists.blocking.blpop("a", "b")""".stripMargin should compile - } - "given two non empty keys and positive timeout" in { - """import auto._ - |lists.blocking.blpop("a", "b", 1)""".stripMargin should compile - } - "given specific read instance" in { - """import auto._ - |case class Foo(x: PosInt) - |implicit val fooRead: Read[protocol.NonNullBulkString, Foo] = Read.instancePF { - | case protocol.NonNullBulkString(ToInt(PosInt(i))) => Foo(i) - |} - |lists.blocking.blpop[Foo]("a", 1)""".stripMargin should compile - } - } - - } - - "using brpop" should { - - "fail to compile" when { - "given empty key" in { - """import auto._ - |lists.blocking.brpop("")""".stripMargin shouldNot compile - } - "given negative timeout" in { - """import auto._ - |lists.blocking.brpop("a", -1)""".stripMargin shouldNot compile - } - "given zero timeout" in { - """import auto._ - |lists.blocking.brpop("a", 0)""".stripMargin shouldNot compile - } - "missing read instance" in { - """import auto._ - |case class Foo(x: Int) - |lists.blocking.brpop[Foo]("a", 1)""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given one non empty key" in { - """import auto._ - |lists.blocking.brpop("a")""".stripMargin should compile - } - "given one non empty key and positive timeout" in { - """import auto._ - |lists.blocking.brpop("a", 1)""".stripMargin should compile - } - "given two non empty keys" in { - """import auto._ - |lists.blocking.brpop("a", "b")""".stripMargin should compile - } - "given two non empty keys and positive timeout" in { - """import auto._ - |lists.blocking.brpop("a", "b", 1)""".stripMargin should compile - } - "given specific read instance" in { - """import auto._ - |case class Foo(x: PosInt) - |implicit val fooRead: Read[protocol.NonNullBulkString, Foo] = Read.instancePF { - | case protocol.NonNullBulkString(ToInt(PosInt(i))) => Foo(i) - |} - |lists.blocking.brpop[Foo]("a", 1)""".stripMargin should compile - } - } - - } - - "using brpoplpush" should { - - "fail to compile" when { - "given empty source key" in { - """import auto._ - |lists.blocking.brpoplpush("", "b")""".stripMargin shouldNot compile - } - "given empty destination key" in { - """import auto._ - |lists.blocking.brpoplpush("a", "")""".stripMargin shouldNot compile - } - "given negative timeout" in { - """import auto._ - |lists.blocking.brpoplpush("a", "b", -1)""".stripMargin shouldNot compile - } - "given zero timeout" in { - """import auto._ - |lists.blocking.brpoplpush("a", "b", 0)""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given non empty source key and non empty destination key" in { - """import auto._ - |lists.blocking.brpoplpush("a", "b")""".stripMargin should compile - } - "given non empty source key, non empty destination key and positive timeout" in { - """import auto._ - |lists.blocking.brpoplpush("a", "b", 1)""".stripMargin should compile - } - } - - } - } -} diff --git a/core/src/test/scala/laserdisc/BaseSpec.scala b/core/src/test/scala/laserdisc/BaseSpec.scala new file mode 100644 index 00000000..ed7c991c --- /dev/null +++ b/core/src/test/scala/laserdisc/BaseSpec.scala @@ -0,0 +1,148 @@ +package laserdisc + +import eu.timepit.refined.api._ +import eu.timepit.refined.scalacheck.reftype.arbitraryRefType +import eu.timepit.refined.scalacheck.{CollectionInstancesBinCompat1, NumericInstances, StringInstances} +import org.scalacheck.{Arbitrary, Gen} +import org.scalacheck.Arbitrary.arbitrary +import org.scalacheck.Gen._ +import org.scalatest.{EitherValues, Matchers, OptionValues, WordSpec} +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks +import scala.Double.{MinValue => DMin, MaxValue => DMax, NaN} +import scala.Int.{MinValue => IMin, MaxValue => IMax} +import scala.Long.{MinValue => LMin, MaxValue => LMax} + +abstract class BaseSpec + extends WordSpec + with Matchers + with EitherValues + with OptionValues + with ScalaCheckPropertyChecks + with CollectionInstancesBinCompat1 + with NumericInstances + with StringInstances { + + private[this] final val dashChar: Char = 0x002D.toChar + private[this] final val dashString: String = dashChar.toString + private[this] final val dotString: String = 0x002E.toChar.toString + private[this] final val spaceChar: Char = 0x0020.toChar + + private[this] final val byteRange: Gen[Int] = chooseNum(0, 255) + private[this] final val dashGen: Gen[Char] = const(dashChar) + private[this] final val hexGen: Gen[Char] = frequency(10 -> numChar, 6 -> choose(0x0061.toChar, 0x0066.toChar)) + private[this] final val spaceGen: Gen[Char] = const(spaceChar) + protected final val utf8BMPCharGen: Gen[Char] = { + val b01 = 24 -> choose(spaceChar, 0x007E.toChar) // 75% it's a 7-bit ASCII char + val b02 = 1 -> choose(0x00A0.toChar, 0x085F.toChar) // 3.125% for all other cases + val b03 = 1 -> choose(0x08A0.toChar, 0x1AAF.toChar) + val b04 = 1 -> choose(0x1B00.toChar, 0x1C7F.toChar) + val b05 = 1 -> choose(0x1CC0.toChar, 0x2FDF.toChar) + val b06 = 1 -> choose(0x2FF0.toChar, 0xA9DF.toChar) + val b07 = 1 -> choose(0xAA00.toChar, 0xAB2F.toChar) + val b08 = 1 -> choose(0xABC0.toChar, 0xD7FF.toChar) + val b09 = 1 -> choose(0xE000.toChar, 0xFFEF.toChar) + + frequency(b01, b02, b03, b04, b05, b06, b07, b08, b09) + } + private[this] final val noSpaceUtf8BMPCharGen: Gen[Char] = utf8BMPCharGen.suchThat(_ != spaceChar) + + private[this] final val globGen: Gen[String] = { + val basicGlob = nonEmptyListOf(frequency(1 -> const('*'), 1 -> const('?'), 1 -> alphaNumChar)).map(_.mkString) + Gen.oneOf(0, 1).flatMap(f => if (f == 0) basicGlob else basicGlob.map(g => s"[$g]")) + } + private[this] final val allNICsGen: Gen[String] = const(AllNICsEqWit.value) + private[this] final val lbGen: Gen[String] = const(LoopbackEqWit.value) + private[this] final val rfc1123Gen: Gen[String] = { + def dashAtBeginOrEnd(s: String) = s.startsWith(dashString) || s.endsWith(dashString) + val piece = chooseNum(1, 62).flatMap(strOfNGen(_, 1 -> dashGen, 99 -> alphaNumChar)) + choose(1, 5).flatMap(listOfN(_, piece.retryUntil(!dashAtBeginOrEnd(_))).map(_.mkString(dotString)).retryUntil(_.size <= 255)) + } + private[this] final val rfc1918Gen: Gen[String] = Gen.oneOf(ipv4Gen(10), chooseNum(15, 31).flatMap(ipv4Gen(172, _)), ipv4Gen(192, 168)) + private[this] final val rfc5737Gen: Gen[String] = Gen.oneOf(ipv4Gen(192, 0, 2), ipv4Gen(198, 51, 100), ipv4Gen(203, 0, 113)) + private[this] final val rfc3927Gen: Gen[String] = ipv4Gen(169, 254) + private[this] final val rfc2544Gen: Gen[String] = chooseNum(18, 19).flatMap(ipv4Gen(198, _)) + + private[this] final def ipv4Gen(hs: Int*): Gen[String] = listOfN(4 - hs.size, byteRange).map(ts => (hs ++: ts).mkString(dotString)) + private[this] final def twoOrMore[A](ga: => Gen[A]): Gen[List[A]] = nonEmptyListOf(ga).suchThat(_.size > 1) + private[this] final def zip[A, B](arbA: => Arbitrary[A], arbB: => Arbitrary[B]): Gen[(A, B)] = + arbA.arbitrary.flatMap(a => arbB.arbitrary.map(a -> _)) + + private[this] final def strGen(lc: Gen[List[Char]]): Gen[String] = lc.map(_.mkString) + private[this] final def strOfNGen(n: Int, fs: (Int, Gen[Char])*): Gen[String] = listOfN(n, frequency(fs: _*)).map(_.mkString) + private[this] final def strOfNSameFreqGen(n: Int, gs: Gen[Char]*): Gen[String] = strOfNGen(n, gs.map(1 -> _): _*) + + final val connectionNameGen: Gen[String] = strGen(nonEmptyListOf(noSpaceUtf8BMPCharGen)) :| "connection name" + final val dbIndexGen: Gen[Int] = chooseNum(0, DbIndexMaxValueWit.value) :| "db index" + final val directionGen: Gen[Direction] = Gen.oneOf(Direction.asc, Direction.desc) :| "direction" + final val geoHashGen: Gen[String] = strOfNGen(11, 1 -> numChar, 9 -> alphaLowerChar) :| "geo hash" + final val globPatternGen: Gen[String] = nonEmptyListOf(globGen).map(_.mkString) :| "glob pattern" + final val hostGen: Gen[String] = Gen.oneOf(allNICsGen, lbGen, rfc1123Gen, rfc1918Gen, rfc5737Gen, rfc3927Gen, rfc2544Gen) :| "host" + final val keyGen: Gen[String] = strGen(nonEmptyListOf(utf8BMPCharGen)) :| "key" + final val latitudeGen: Gen[Double] = chooseNum(LatitudeMinValueWit.value, LatitudeMaxValueWit.value) :| "latitude" + final val longitudeGen: Gen[Double] = chooseNum(LongitudeMinValueWit.value, LongitudeMaxValueWit.value) :| "longitude" + final val nodeIdGen: Gen[String] = strOfNSameFreqGen(40, hexGen) :| "node id" + final val nonNegDoubleGen: Gen[Double] = chooseNum(0.0D, DMax) :| "double >= 0.0D" + final val nonNegIntGen: Gen[Int] = chooseNum(0, IMax) :| "int >= 0" + final val nonNegLongGen: Gen[Long] = chooseNum(0L, LMax) :| "long >= 0L" + final val nonZeroDoubleGen: Gen[Double] = chooseNum(DMin, DMax).suchThat(d => d != 0.0D && d != NaN) :| "double != 0.0D and != NaN" + final val nonZeroIntGen: Gen[Int] = chooseNum(IMin, IMax).suchThat(_ != 0) :| "int != 0" + final val nonZeroLongGen: Gen[Long] = chooseNum(LMin, LMax).suchThat(_ != 0L) :| "long != 0L" + final val portGen: Gen[Int] = chooseNum(PortMinValueWit.value, PortMaxValueWit.value) :| "port" + final val rangeOffsetGen: Gen[Int] = chooseNum(0, RangeOffsetMaxValueWit.value) :| "range offset" + final val slotGen: Gen[Int] = chooseNum(0, SlotMaxValueWit.value) :| "slot" + final val stringLengthGen: Gen[Long] = chooseNum(0L, StringLengthMaxValueWit.value) :| "string length" + final val stringsWithSpacesGen: Gen[String] = strGen(listOf(frequency(1 -> spaceGen, 10 -> noSpaceUtf8BMPCharGen))) :| "string w/ spaces" + final val validDoubleGen: Gen[Double] = chooseNum(DMin, DMax).suchThat(_ != NaN) :| "double != NaN" + + final val connectionNameIsValid: String => Boolean = Validate[String, ConnectionNameRef].isValid + final val dbIndexIsValid: Int => Boolean = Validate[Int, DbIndexRef].isValid + final val geoHashIsValid: String => Boolean = Validate[String, GeoHashRef].isValid + final val globPatternIsValid: String => Boolean = Validate[String, GlobPatternRef].isValid + final val hostIsValid: String => Boolean = Validate[String, HostRef].isValid + final val keyIsValid: String => Boolean = Validate[String, KeyRef].isValid + final val latitudeIsValid: Double => Boolean = Validate[Double, LatitudeRef].isValid + final val longitudeIsValid: Double => Boolean = Validate[Double, LongitudeRef].isValid + final val nodeIdIsValid: String => Boolean = Validate[String, NodeIdRef].isValid + final val nonNegDoubleIsValid: Double => Boolean = Validate[Double, NonNegDoubleRef].isValid + final val nonNegIntIsValid: Int => Boolean = Validate[Int, NonNegRef].isValid + final val nonNegLongIsValid: Long => Boolean = Validate[Long, NonNegRef].isValid + final val nonZeroDoubleIsValid: Double => Boolean = Validate[Double, NonZeroDoubleRef].isValid + final val nonZeroIntIsValid: Int => Boolean = Validate[Int, NonZeroIntRef].isValid + final val nonZeroLongIsValid: Long => Boolean = Validate[Long, NonZeroLongRef].isValid + final def oneOrMoreIsValid[A]: List[A] => Boolean = Validate[List[A], OneOrMoreRef].isValid + final val oneOrMoreKeysIsValid: List[Key] => Boolean = Validate[List[Key], OneOrMoreRef].isValid + final val portIsValid: Int => Boolean = Validate[Int, PortRef].isValid + final val rangeOffsetIsValid: Int => Boolean = Validate[Int, RangeOffsetRef].isValid + final val slotIsValid: Int => Boolean = Validate[Int, SlotRef].isValid + final val stringLengthIsValid: Long => Boolean = Validate[Long, StringLengthRef].isValid + final val twoOrMoreKeysIsValid: List[Key] => Boolean = Validate[List[Key], TwoOrMoreRef].isValid + final val twoOrMoreWeightedKeysIsValid: List[(Key, ValidDouble)] => Boolean = Validate[List[(Key, ValidDouble)], TwoOrMoreRef].isValid + final val validDoubleIsValid: Double => Boolean = Validate[Double, ValidDoubleRef].isValid + + implicit final val connectionNameArb: Arbitrary[ConnectionName] = arbitraryRefType(connectionNameGen) + implicit final val directionArb: Arbitrary[Direction] = Arbitrary(directionGen) + implicit final val geoHashArb: Arbitrary[GeoHash] = arbitraryRefType(geoHashGen) + implicit final val globPatternArb: Arbitrary[GlobPattern] = arbitraryRefType(globPatternGen) + implicit final val hostArb: Arbitrary[Host] = arbitraryRefType(hostGen) + implicit final val indexArb: Arbitrary[Index] = arbitraryRefType(arbitrary[Long]) + implicit final val keyArb: Arbitrary[Key] = arbitraryRefType(keyGen) + implicit final def kvArb[A](implicit A: Arbitrary[A]): Arbitrary[KV[A]] = + Arbitrary(keyArb.arbitrary.flatMap(k => A.arbitrary.map(KV(k, _)))) + implicit final val nodeIdArb: Arbitrary[NodeId] = arbitraryRefType(nodeIdGen) + implicit final val nonNegDoubleArb: Arbitrary[NonNegDouble] = arbitraryRefType(nonNegDoubleGen) + implicit final val nonNegIntArb: Arbitrary[NonNegInt] = arbitraryRefType(nonNegIntGen) + implicit final val nonNegLongArb: Arbitrary[NonNegLong] = arbitraryRefType(nonNegLongGen) + implicit final val nonZeroDoubleArb: Arbitrary[NonZeroDouble] = arbitraryRefType(nonZeroDoubleGen) + implicit final val nonZeroIntArb: Arbitrary[NonZeroInt] = arbitraryRefType(nonZeroIntGen) + implicit final val nonZeroLongArb: Arbitrary[NonZeroLong] = arbitraryRefType(nonZeroLongGen) + implicit final val scanKeyArb: Arbitrary[Scan[Key]] = + Arbitrary(nonNegLongArb.arbitrary.flatMap(l => option(listOf(keyArb.arbitrary)).map(Scan(l, _)))) + implicit final val scanKVArb: Arbitrary[ScanKV] = + Arbitrary(nonNegLongArb.arbitrary.flatMap(l => option(listOf(kvArb[String].arbitrary)).map(ScanKV(l, _)))) + implicit final val slotArb: Arbitrary[Slot] = arbitraryRefType(slotGen) + implicit final val twoOrMoreKeysArb: Arbitrary[TwoOrMoreKeys] = arbitraryRefType(twoOrMore(keyArb.arbitrary)) + implicit final val twoOrMoreWeightedKeysArb: Arbitrary[TwoOrMoreWeightedKeys] = arbitraryRefType(twoOrMore(zip(keyArb, validDoubleArb))) + implicit final val validDoubleArb: Arbitrary[ValidDouble] = arbitraryRefType(validDoubleGen) + + final val boolToNum: Boolean => Num = b => Num(if (b) 1 else 0) +} diff --git a/core/src/test/scala/laserdisc/ConnectionPSpec.scala b/core/src/test/scala/laserdisc/ConnectionPSpec.scala deleted file mode 100644 index 47a9e296..00000000 --- a/core/src/test/scala/laserdisc/ConnectionPSpec.scala +++ /dev/null @@ -1,126 +0,0 @@ -package laserdisc - -import org.scalatest.{Matchers, WordSpecLike} - -final class ConnectionPSpec extends WordSpecLike with Matchers { - - "A ConnectionP" when { - - "using auth" should { - - "fail to compile" when { - "given empty password" in { - """import auto._ - |connection.auth("")""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given non empty password" in { - """import auto._ - |connection.auth("a")""".stripMargin should compile - } - } - - } - - "using echo" should { - - "fail to compile" when { - "given empty message" in { - """import auto._ - |connection.echo("")""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given non empty message" in { - """import auto._ - |connection.echo("a")""".stripMargin should compile - } - } - - } - - "using ping" should { - - "fail to compile" when { - "given empty message" in { - """import auto._ - |connection.ping("")""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given non empty message" in { - """import auto._ - |connection.ping("a")""".stripMargin should compile - } - "using val for empty message" in { - """connection.ping""".stripMargin should compile - } - } - - } - - "using quit" should { - - "always compile" in { - """connection.quit""".stripMargin should compile - } - - } - - "using select" should { - - "fail to compile" when { - "given out of bounds index (less than min: -1)" in { - """import auto._ - |connection.select(-1)""".stripMargin shouldNot compile - } - "given out of bounds index (greater than max: 16)" in { - """import auto._ - |connection.select(16)""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given valid index (min: 0)" in { - """import auto._ - |connection.select(0)""".stripMargin should compile - } - "given valid index (max: 15)" in { - """import auto._ - |connection.select(15)""".stripMargin should compile - } - } - - } - - "using swapdb" should { - - "fail to compile" when { - "given out of bounds indexes (index1 less than min: -1, index2 greater than max: 16)" in { - """import auto._ - |connection.swapdb(-1, 16)""".stripMargin shouldNot compile - } - "given out of bounds indexes (index1 greater than max: 16, index2 less than min: -1)" in { - """import auto._ - |connection.swapdb(16, -1)""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given valid indexes (index1 min: 0, index2 max: 15)" in { - """import auto._ - |connection.swapdb(0, 15)""".stripMargin should compile - } - "given valid indexes (index1 max: 15, index2 min: 0)" in { - """import auto._ - |connection.swapdb(15, 0)""".stripMargin should compile - } - } - - } - } -} diff --git a/core/src/test/scala/laserdisc/HyperLogLogPSpec.scala b/core/src/test/scala/laserdisc/HyperLogLogPSpec.scala deleted file mode 100644 index 08ec030e..00000000 --- a/core/src/test/scala/laserdisc/HyperLogLogPSpec.scala +++ /dev/null @@ -1,61 +0,0 @@ -package laserdisc - -import org.scalatest.{Matchers, WordSpecLike} - -final class HyperLogLogPSpec extends WordSpecLike with Matchers { - - "A HyperLogLogP" when { - - "using pfadd" should { - - "fail to compile" when { - "given empty key" in { - """import auto._ - |hyperloglog.pfadd("", "e")""".stripMargin shouldNot compile - } - "given empty element" in { - """import auto._ - |hyperloglog.pfadd("a", "")""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given non empty key and one non empty element" in { - """import auto._ - |hyperloglog.pfadd("a", "e1")""".stripMargin should compile - } - "given non empty key and two non empty elements" in { - """import auto._ - |hyperloglog.pfadd("a", "e1", "e2")""".stripMargin should compile - } - } - - } - - "using pfcount" should { - - "fail to compile" when { - "given one empty key" in { - """import auto._ - |hyperloglog.pfcount("")""".stripMargin shouldNot compile - } - "given two empty keys" in { - """import auto._ - |hyperloglog.pfcount("", "")""".stripMargin shouldNot compile - } - } - - "compile successfully" when { - "given one non empty key" in { - """import auto._ - |hyperloglog.pfcount("a")""".stripMargin should compile - } - "given two non empty keys" in { - """import auto._ - |hyperloglog.pfcount("a", "b")""".stripMargin should compile - } - } - - } - } -} diff --git a/core/src/test/scala/laserdisc/RefinedTypesSpec.scala b/core/src/test/scala/laserdisc/RefinedTypesSpec.scala new file mode 100644 index 00000000..30b958ff --- /dev/null +++ b/core/src/test/scala/laserdisc/RefinedTypesSpec.scala @@ -0,0 +1,778 @@ +package laserdisc + +final class RefinedTypesSpec extends BaseSpec { + + "ConnectionName" should { + + "fail to compile" when { + "given an empty String" in { + """ConnectionName("")""" shouldNot compile + } + "given a space" in { + """ConnectionName(" ")""" shouldNot compile + } + } + + "fail at runtime" when { + "provided non literal cases of Strings that contain spaces" in forAll(stringsWithSpacesGen) { s => + whenever(!connectionNameIsValid(s)) { + an[IllegalArgumentException] should be thrownBy ConnectionName.unsafeFrom(s) + } + } + } + + "compile" when { + "given non empty String with no spaces" in { + """ConnectionName("a")""" should compile + } + } + + "refine correctly" when { + "provided non literal cases of non empty Strings with no spaces" in forAll(connectionNameGen) { s => + whenever(connectionNameIsValid(s)) { + ConnectionName.from(s).right.value.value shouldBe s + } + } + } + } + + "DbIndex" should { + + "fail to compile" when { + "given out of range Int (< 0)" in { + "DbIndex(-1)" shouldNot compile + } + "given out of range Int (> 15)" in { + "DbIndex(16)" shouldNot compile + } + } + + "fail at runtime" when { + "provided non literal cases of out of range Ints" in forAll { (i: Int) => + whenever(!dbIndexIsValid(i)) { + an[IllegalArgumentException] should be thrownBy DbIndex.unsafeFrom(i) + } + } + } + + "compile" when { + "given in range Int" in { + "DbIndex(0)" should compile + } + } + + "refine correctly" when { + "provided non literal cases of in range Ints" in forAll(dbIndexGen) { i => + whenever(dbIndexIsValid(i)) { + DbIndex.from(i).right.value.value shouldBe i + } + } + } + } + + "GeoHash" should { + + "fail to compile" when { + "given a non conformant String (length < 11)" in { + """GeoHash("abcdefghij")""" shouldNot compile + } + "given a non conformant String (length > 11)" in { + """GeoHash("abcdefghijkl")""" shouldNot compile + } + "given a non conformant String (uppercase)" in { + """GeoHash("abCdefghijk")""" shouldNot compile + } + "given a non conformant String (invalid chars)" in { + """GeoHash("abcd&fghijk")""" shouldNot compile + } + } + + "fail at runtime" when { + "provided non literal cases of non conformant Strings" in forAll { (s: String) => + whenever(!geoHashIsValid(s)) { + an[IllegalArgumentException] should be thrownBy GeoHash.unsafeFrom(s) + } + } + } + + "compile" when { + "given conformant String" in { + """GeoHash("abcd3fgh1jk")""" should compile + } + } + + "refine correctly" when { + "provided non literal cases of conformant Strings" in forAll(geoHashGen) { s => + whenever(geoHashIsValid(s)) { + GeoHash.from(s).right.value.value shouldBe s + } + } + } + } + + "GlobPattern" should { + + "fail to compile" when { + "given an empty String" in { + """GlobPattern("")""" shouldNot compile + } + "given a non conformant String" in { + """GlobPattern("!")""" shouldNot compile + } + } + + "fail at runtime" when { + "provided non literal cases of non conformant Strings" in forAll { (s: String) => + whenever(!globPatternIsValid(s)) { + an[IllegalArgumentException] should be thrownBy GlobPattern.unsafeFrom(s) + } + } + } + + "compile" when { + "given conformant String" in { + """GlobPattern("abc*fg?1jk")""" should compile + """GlobPattern("a[bc*]fg?1jk")""" should compile + } + } + + "refine correctly" when { + "provided non literal cases of conformant Strings" in forAll(globPatternGen) { s => + whenever(globPatternIsValid(s)) { + GlobPattern.from(s).right.value.value shouldBe s + } + } + } + } + + "Host" should { + + "fail to compile" when { + "given an empty String" in { + """Host("")""" shouldNot compile + } + "given a dash-ending hostname (RFC-1123)" in { + """Host("A0c-")""" shouldNot compile + } + "given a dash-beginning hostname (RFC-1123)" in { + """Host("-A0c")""" shouldNot compile + } + "given a hostname label whose length is > 63 (RFC-1123)" in { + """Host("o123456701234567012345670123456701234567012345670123456701234567")""" shouldNot compile + } + "given a hostname whose length is > 255 (RFC-1123)" in { + """Host( + "o12345670123456701234567012345670123456701234567012345670123456" + + ".o12345670123456701234567012345670123456701234567012345670123456" + + ".o12345670123456701234567012345670123456701234567012345670123456" + + ".o12345670123456701234567012345670123456701234567012345670123456" + + ".a" + )""" shouldNot compile + } + "given an invalid (non-private) IP address" in { + """Host("1.1.1.1")""" shouldNot compile + } + } + + "compile" when { + "given all NICs (0.0.0.0)" in { + """Host("0.0.0.0")""" should compile + } + "given loopback address (127.0.0.1)" in { + """Host("127.0.0.1")""" should compile + } + "given localhost (RFC-1123)" in { + """Host("localhost")""" should compile + } + "given domain.local (RFC-1123)" in { + """Host("domain.local")""" should compile + } + "given any digit hostname (RFC-1123)" in { + """Host("01234")""" should compile + } + "given any dash inside hostname (RFC-1123)" in { + """Host("01234-abc")""" should compile + } + "given 10. IPv4 (RFC-1918)" in { + """Host("10.1.2.3")""" should compile + } + "given 172.(15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31). IPv4 (RFC-1918)" in { + """Host("10.15.1.2")""" should compile + """Host("10.16.1.2")""" should compile + """Host("10.17.1.2")""" should compile + """Host("10.18.1.2")""" should compile + """Host("10.19.1.2")""" should compile + """Host("10.20.1.2")""" should compile + """Host("10.21.1.2")""" should compile + """Host("10.22.1.2")""" should compile + """Host("10.23.1.2")""" should compile + """Host("10.24.1.2")""" should compile + """Host("10.25.1.2")""" should compile + """Host("10.26.1.2")""" should compile + """Host("10.27.1.2")""" should compile + """Host("10.28.1.2")""" should compile + """Host("10.29.1.2")""" should compile + """Host("10.30.1.2")""" should compile + """Host("10.31.1.2")""" should compile + } + "given 192.168. IPv4 (RFC-1918)" in { + """Host("192.168.1.2")""" should compile + } + "given 192.0.2. IPv4 (RFC-5737)" in { + """Host("192.0.2.1")""" should compile + } + "given 198.51.100. IPv4 (RFC-5737)" in { + """Host("198.51.100.1")""" should compile + } + "given 203.0.113. IPv4 (RFC-5737)" in { + """Host("203.0.113.1")""" should compile + } + "given 169.254. IPv4 (RFC-3927)" in { + """Host("169.254.1.2")""" should compile + } + "given 198.(18|19). IPv4 (RFC-2544)" in { + """Host("198.18.1.2")""" should compile + """Host("198.19.1.2")""" should compile + } + } + + "refine correctly" when { + "provided non literal cases of valid hosts" in forAll(hostGen) { s => + whenever(hostIsValid(s)) { + Host.from(s).right.value.value shouldBe s + } + } + } + } + + "Key" should { + + "fail to compile" when { + "given an empty String" in { + """Key("")""" shouldNot compile + } + } + + "compile" when { + "given a non empty String" in { + """Key("a")""" should compile + } + } + + "refine correctly" when { + "provided non literal cases of non empty Strings" in forAll { (s: String) => + whenever(keyIsValid(s)) { + Key.from(s).right.value.value shouldBe s + } + } + } + } + + "Latitude" should { + + "fail to compile" when { + "given out of range Double (< -85.05112878D)" in { + "Latitude(-85.05112879D)" shouldNot compile + } + "given out of range Double (> 85.05112878D)" in { + "Latitude(85.05112879D)" shouldNot compile + } + } + + "fail at runtime" when { + "provided non literal cases of out of range Doubles (d < -85.05112878D | d > 85.05112878D)" in forAll { (d: Double) => + whenever(!latitudeIsValid(d)) { + an[IllegalArgumentException] should be thrownBy Latitude.unsafeFrom(d) + } + } + } + + "compile" when { + "given edge cases (-85.05112878D)" in { + "Latitude(-85.05112878D)" should compile + } + "given edge cases (85.05112878D)" in { + "Latitude(85.05112878D)" should compile + } + } + + "refine correctly" when { + "provided non literal cases of in range Doubles (-85.05112878D <= d <= 85.05112878D)" in forAll(latitudeGen) { d => + whenever(latitudeIsValid(d)) { + Latitude.from(d).right.value.value shouldBe d + } + } + } + } + + "Longitude" should { + + "fail to compile" when { + "given out of range Double (< -180.0D)" in { + "Longitude(-180.00000001D)" shouldNot compile + } + "given out of range Double (> 180.0D)" in { + "Longitude(180.00000001D)" shouldNot compile + } + } + + "fail at runtime" when { + "provided non literal cases of out of range Doubles (d < -180.0D | d > 180.0D)" in forAll { (d: Double) => + whenever(!longitudeIsValid(d)) { + an[IllegalArgumentException] should be thrownBy Longitude.unsafeFrom(d) + } + } + } + + "compile" when { + "given edge cases (-180.0D)" in { + "Longitude(-180.0D)" should compile + } + "given edge cases (180.0D)" in { + "Longitude(180.0D)" should compile + } + } + + "refine correctly" when { + "provided non literal cases of in range Doubles (-180.0D <= d <= 180.0D)" in forAll(longitudeGen) { d => + whenever(longitudeIsValid(d)) { + Longitude.from(d).right.value.value shouldBe d + } + } + } + } + + "NodeId" should { + + "fail to compile" when { + "given a non conformant String (length < 40)" in { + """NodeId("0123456789abcdef0123456789abcdef0123456")""" shouldNot compile + } + "given a non conformant String (length > 40)" in { + """NodeId("0123456789abcdef0123456789abcdef012345678")""" shouldNot compile + } + "given a non conformant String (uppercase)" in { + """NodeId("0123456789abcdEf0123456789abcdef01234567")""" shouldNot compile + } + "given a non conformant String (invalid chars)" in { + """NodeId("0123456789abcd&f0123456789abcdef01234567&fghijk")""" shouldNot compile + } + } + + "fail at runtime" when { + "provided non literal cases of non conformant Strings" in forAll { (s: String) => + whenever(!nodeIdIsValid(s)) { + an[IllegalArgumentException] should be thrownBy NodeId.unsafeFrom(s) + } + } + } + + "compile" when { + "given conformant String" in { + """NodeId("0123456789abcdef0123456789abcdef01234567")""" should compile + } + } + + "refine correctly" when { + "provided non literal cases of conformant Strings" in forAll(nodeIdGen) { s => + whenever(nodeIdIsValid(s)) { + NodeId.from(s).right.value.value shouldBe s + } + } + } + } + + "NonNegDouble" should { + + "fail to compile" when { + "given out of range Double (< 0.0D)" in { + "NonNegDouble(-0.00000001D)" shouldNot compile + } + "given NaN Double" in { + "NonNegDouble(Double.NaN)" shouldNot compile + } + } + + "fail at runtime" when { + "provided non literal cases of out of range Doubles (d < 0.0D)" in forAll { (d: Double) => + whenever(!nonNegDoubleIsValid(d)) { + an[IllegalArgumentException] should be thrownBy NonNegDouble.unsafeFrom(d) + } + } + } + + "compile" when { + "given edge cases (0.0D)" in { + "NonNegDouble(0.0D)" should compile + } + } + + "refine correctly" when { + "provided non literal cases of in range Doubles (d >= 0.0D)" in forAll(nonNegDoubleGen) { d => + whenever(nonNegDoubleIsValid(d)) { + NonNegDouble.from(d).right.value.value shouldBe d + } + } + } + } + + "NonNegInt" should { + + "fail to compile" when { + "given out of range Ints (< 0)" in { + "NonNegInt(-1)" shouldNot compile + } + } + + "fail at runtime" when { + "provided non literal cases of out of range Ints (i < 0)" in forAll { (i: Int) => + whenever(!nonNegIntIsValid(i)) { + an[IllegalArgumentException] should be thrownBy NonNegInt.unsafeFrom(i) + } + } + } + + "compile" when { + "given edge cases (0)" in { + "NonNegInt(0)" should compile + } + } + + "refine correctly" when { + "provided non literal cases of in range Ints (i > 0)" in forAll(nonNegIntGen) { i => + whenever(nonNegIntIsValid(i)) { + NonNegInt.from(i).right.value.value shouldBe i + } + } + } + } + + "NonNegLong" should { + + "fail to compile" when { + "given out of range Longs (< 0L)" in { + "NonNegLong(-1L)" shouldNot compile + } + } + + "fail at runtime" when { + "provided non literal cases of out of range Longs (l < 0L)" in forAll { (l: Long) => + whenever(!nonNegLongIsValid(l)) { + an[IllegalArgumentException] should be thrownBy NonNegLong.unsafeFrom(l) + } + } + } + + "compile" when { + "given edge cases (0L)" in { + "NonNegLong(0L)" should compile + } + } + + "refine correctly" when { + "provided non literal cases of in range Longs (l > 0L)" in forAll(nonNegLongGen) { l => + whenever(nonNegLongIsValid(l)) { + NonNegLong.from(l).right.value.value shouldBe l + } + } + } + } + + "NonZeroDouble" should { + + "fail to compile" when { + "given 0.0D" in { + "NonZeroDouble(0.0D)" shouldNot compile + } + "given NaN" in { + "NonZeroDouble(Double.NaN)" shouldNot compile + } + } + + "refine correctly" when { + "provided non literal cases of valid Doubles (d != 0.0D)" in forAll(nonZeroDoubleGen) { d => + whenever(nonZeroDoubleIsValid(d)) { + NonZeroDouble.from(d).right.value.value shouldBe d + } + } + } + } + + "NonZeroInt" should { + + "fail to compile" when { + "given 0" in { + "NonZeroInt(0)" shouldNot compile + } + } + + "refine correctly" when { + "provided non literal cases of valid Ints (i != 0)" in forAll(nonZeroIntGen) { i => + whenever(nonZeroIntIsValid(i)) { + NonZeroInt.from(i).right.value.value shouldBe i + } + } + } + } + + "NonZeroLong" should { + + "fail to compile" when { + "given 0L" in { + "NonZeroLong(0L)" shouldNot compile + } + } + + "refine correctly" when { + "provided non literal cases of valid Longs (l != 0L)" in forAll(nonZeroLongGen) { l => + whenever(nonZeroLongIsValid(l)) { + NonZeroLong.from(l).right.value.value shouldBe l + } + } + } + } + + "OneOrMore" should { + + "fail at runtime" when { + "provided empty List" in { + an[IllegalArgumentException] should be thrownBy OneOrMore.unsafeFrom(List.empty[Int]) + } + } + + "refine correctly" when { + "provided non literal cases of non empty Lists (length > 0)" in forAll { (is: List[Int]) => + whenever(is.nonEmpty) { + OneOrMore.from(is).right.value.value shouldBe is + } + } + } + } + + "OneOrMoreKeys" should { + + "fail to compile" when { + "given non literal empty List" in { + "OneOrMoreKeys(List.empty)" shouldNot compile + } + "given non literal non empty List" in { + """OneOrMoreKeys(List(Key("a")))""" shouldNot compile + } + } + + "fail at runtime" when { + "provided empty List" in { + an[IllegalArgumentException] should be thrownBy OneOrMoreKeys.unsafeFrom(List.empty) + } + } + + "refine correctly" when { + "provided non literal cases of non empty Lists (length > 0)" in forAll { (ks: List[Key]) => + whenever(ks.nonEmpty) { + OneOrMoreKeys.from(ks).right.value.value shouldBe ks + } + } + } + } + + "RangeOffset" should { + + "fail to compile" when { + "given out of range Int (< 0)" in { + "RangeOffset(-1)" shouldNot compile + } + "given out of range Int (> 536870911)" in { + "RangeOffset(536870912)" shouldNot compile + } + } + + "fail at runtime" when { + "provided non literal cases of out of range Ints (i < 0 | i > 536870911)" in forAll { (i: Int) => + whenever(!rangeOffsetIsValid(i)) { + an[IllegalArgumentException] should be thrownBy RangeOffset.unsafeFrom(i) + } + } + } + + "compile" when { + "given edge cases (0)" in { + "RangeOffset(0)" should compile + } + "given edge cases (536870911)" in { + "RangeOffset(536870911)" should compile + } + } + + "refine correctly" when { + "provided non literal cases of in range Ints (0 <= i <= 536870911)" in forAll(rangeOffsetGen) { i => + whenever(rangeOffsetIsValid(i)) { + RangeOffset.from(i).right.value.value shouldBe i + } + } + } + } + + "Slot" should { + + "fail to compile" when { + "given out of range Int (< 0)" in { + "Slot(-1)" shouldNot compile + } + "given out of range Int (> 16383)" in { + "Slot(16384)" shouldNot compile + } + } + + "fail at runtime" when { + "provided non literal cases of out of range Ints (i < 0 | i > 16383)" in forAll { (i: Int) => + whenever(!slotIsValid(i)) { + an[IllegalArgumentException] should be thrownBy Slot.unsafeFrom(i) + } + } + } + + "compile" when { + "given edge cases (0)" in { + "Slot(0)" should compile + } + "given edge cases (16383)" in { + "Slot(16383)" should compile + } + } + + "refine correctly" when { + "provided non literal cases of in range Ints (0 <= i <= 16383)" in forAll(slotGen) { i => + whenever(slotIsValid(i)) { + Slot.from(i).right.value.value shouldBe i + } + } + } + } + + "StringLength" should { + + "fail to compile" when { + "given out of range Long (< 0L)" in { + "StringLength(-1L)" shouldNot compile + } + "given out of range Long (> 4294967295L)" in { + "StringLength(4294967296L)" shouldNot compile + } + } + + "fail at runtime" when { + "provided non literal cases of out of range Longs (l < 0L | l > 4294967295L)" in forAll { (l: Long) => + whenever(!stringLengthIsValid(l)) { + an[IllegalArgumentException] should be thrownBy StringLength.unsafeFrom(l) + } + } + } + + "compile" when { + "given edge cases (0L)" in { + "StringLength(0L)" should compile + } + "given edge cases (4294967295L)" in { + "StringLength(4294967295L)" should compile + } + } + + "refine correctly" when { + "provided non literal cases of in range Longs (0L <= l <= 4294967295L)" in forAll(stringLengthGen) { l => + whenever(stringLengthIsValid(l)) { + StringLength.from(l).right.value.value shouldBe l + } + } + } + } + + "TwoOrMoreKeys" should { + + "fail to compile" when { + "given non literal empty List" in { + "TwoOrMoreKeys(List.empty)" shouldNot compile + } + "given non literal single element List" in { + """TwoOrMoreKeys(List(Key("a")))""" shouldNot compile + } + "given non literal List of two elements" in { + """TwoOrMoreKeys(List(Key("a"), Key("b")))""" shouldNot compile + } + } + + "fail at runtime" when { + "provided empty List" in { + an[IllegalArgumentException] should be thrownBy TwoOrMoreKeys.unsafeFrom(List.empty) + } + "provided single element List" in { + an[IllegalArgumentException] should be thrownBy TwoOrMoreKeys.unsafeFrom(List(Key("a"))) + } + } + + "refine correctly" when { + "provided non literal cases of Lists of length > 1" in forAll { (ks: List[Key]) => + whenever(ks.size > 1) { + TwoOrMoreKeys.from(ks).right.value.value shouldBe ks + } + } + } + } + + "TwoOrMoreWeightedKeys" should { + + "fail to compile" when { + "given non literal empty List" in { + "TwoOrMoreWeightedKeys(List.empty)" shouldNot compile + } + "given non literal single element List" in { + """TwoOrMoreWeightedKeys(List(Key("a") -> ValidDouble(42.0D)))""" shouldNot compile + } + "given non literal List of two elements" in { + """TwoOrMoreWeightedKeys(List(Key("a") -> ValidDouble(42.0D), Key("b") -> ValidDouble(23.0D)))""" shouldNot compile + } + } + + "fail at runtime" when { + "provided empty List" in { + an[IllegalArgumentException] should be thrownBy TwoOrMoreWeightedKeys.unsafeFrom(List.empty) + } + "provided single element List" in { + an[IllegalArgumentException] should be thrownBy TwoOrMoreWeightedKeys.unsafeFrom(List(Key("a") -> ValidDouble(42.0D))) + } + } + + "refine correctly" when { + "provided non literal cases of Lists of length > 1" in forAll { (kvds: List[(Key, ValidDouble)]) => + whenever(kvds.size > 1) { + TwoOrMoreWeightedKeys.from(kvds).right.value.value shouldBe kvds + } + } + } + } + + "ValidDouble" should { + + "fail to compile" when { + "given Double.NaN" in { + "ValidDouble(Double.NaN)" shouldNot compile + } + } + + "compile" when { + "given edge cases (-1.7976931348623157E308) -> can't use Double.MinValue as not a literal" in { + "ValidDouble(-1.7976931348623157E308)" should compile + } + "given edge cases (Double.MaxValue)" in { + "ValidDouble(Double.MaxValue)" should compile + } + } + + "refine correctly" when { + "provided non literal cases of valid Doubles (d != Double.NaN)" in forAll { (d: Double) => + whenever(validDoubleIsValid(d)) { + ValidDouble.from(d).right.value.value shouldBe d + } + } + } + } +} diff --git a/core/src/test/scala/laserdisc/protocol/BListPSpec.scala b/core/src/test/scala/laserdisc/protocol/BListPSpec.scala new file mode 100644 index 00000000..ddb7d8b9 --- /dev/null +++ b/core/src/test/scala/laserdisc/protocol/BListPSpec.scala @@ -0,0 +1,96 @@ +package laserdisc +package protocol + +final class BListPSpec extends BListExtPSpec { + + "The Blocking List protocol" when { + + "using blpop" should { + + "fail to compile" when { + "given one key and timeout but missing read instance" in { + """blpop[Bar](OneOrMoreKeys.unsafeFrom(List(Key("a"))), NonNegInt(0))""" shouldNot compile + } + } + + "roundtrip successfully" when { + "given one or more keys and timeout" in forAll("keys", "timeout", "returned value") { (ks: OneOrMoreKeys, nni: NonNegInt, i: Int) => + val protocol = blpop[Int](ks, nni) + + protocol.encode shouldBe Arr((Bulk("BLPOP") :: ks.value.map(Bulk(_))) :+ Bulk(nni)) + protocol.decode(Arr(Bulk(ks.value.headOption.value), Bulk(i))).right.value.value shouldBe KV(ks.value.headOption.value, i) + } + "given one or more keys, timeout and specific read instance" in { + forAll("keys", "timeout", "returned value") { (ks: OneOrMoreKeys, nni: NonNegInt, i: Int) => + val protocol = blpop[Foo](ks, nni) + + protocol.encode shouldBe Arr((Bulk("BLPOP") :: ks.value.map(Bulk(_))) :+ Bulk(nni)) + protocol.decode(Arr(Bulk(ks.value.headOption.value), Bulk(i))).right.value.value shouldBe KV(ks.value.headOption.value, Foo(i)) + } + } + } + } + + "using brpop" should { + + "fail to compile" when { + "given one key and timeout but missing read instance" in { + """brpop[Bar](OneOrMoreKeys.unsafeFrom(List(Key("a"))), NonNegInt(0))""" shouldNot compile + } + } + + "roundtrip successfully" when { + "given one or more keys and timeout" in forAll("keys", "timeout", "returned value") { (ks: OneOrMoreKeys, nni: NonNegInt, i: Int) => + val protocol = brpop[Int](ks, nni) + + protocol.encode shouldBe Arr((Bulk("BRPOP") :: ks.value.map(Bulk(_))) :+ Bulk(nni)) + protocol.decode(Arr(Bulk(ks.value.headOption.value), Bulk(i))).right.value.value shouldBe KV(ks.value.headOption.value, i) + } + "given one or more keys, timeout and specific read instance" in { + forAll("keys", "timeout", "returned value") { (ks: OneOrMoreKeys, nni: NonNegInt, i: Int) => + val protocol = brpop[Foo](ks, nni) + + protocol.encode shouldBe Arr((Bulk("BRPOP") :: ks.value.map(Bulk(_))) :+ Bulk(nni)) + protocol.decode(Arr(Bulk(ks.value.headOption.value), Bulk(i))).right.value.value shouldBe KV(ks.value.headOption.value, Foo(i)) + } + } + } + } + + "using brpoplpush" should { + + "roundtrip successfully" when { + "given source key and destination key" in forAll("source key", "destination key", "returned value") { (s: Key, d: Key, i: Int) => + val protocol = brpoplpush[Int](s, d) + + protocol.encode shouldBe Arr(Bulk("BRPOPLPUSH"), Bulk(s), Bulk(d), Bulk(0)) + protocol.decode(Bulk(i)).right.value.value shouldBe i + } + "given source key, destination key and specific read instance" in { + forAll("source key", "destination key", "returned value") { (s: Key, d: Key, i: Int) => + val protocol = brpoplpush[Foo](s, d) + + protocol.encode shouldBe Arr(Bulk("BRPOPLPUSH"), Bulk(s), Bulk(d), Bulk(0)) + protocol.decode(Bulk(i)).right.value.value shouldBe Foo(i) + } + } + "given source key, destination key and timeout" in { + forAll("source key", "destination key", "timeout", "returned value") { (s: Key, d: Key, pi: PosInt, i: Int) => + val protocol = brpoplpush[Int](s, d, pi) + + protocol.encode shouldBe Arr(Bulk("BRPOPLPUSH"), Bulk(s), Bulk(d), Bulk(pi)) + protocol.decode(Bulk(i)).right.value.value shouldBe i + } + } + "given source key, destination key, timeout and specific read instance" in { + forAll("source key", "destination key", "timeout", "returned value") { (s: Key, d: Key, pi: PosInt, i: Int) => + val protocol = brpoplpush[Foo](s, d, pi) + + protocol.encode shouldBe Arr(Bulk("BRPOPLPUSH"), Bulk(s), Bulk(d), Bulk(pi)) + protocol.decode(Bulk(i)).right.value.value shouldBe Foo(i) + } + } + } + } + } +} diff --git a/core/src/test/scala/laserdisc/protocol/ClusterPSpec.scala b/core/src/test/scala/laserdisc/protocol/ClusterPSpec.scala new file mode 100644 index 00000000..88d302c0 --- /dev/null +++ b/core/src/test/scala/laserdisc/protocol/ClusterPSpec.scala @@ -0,0 +1,421 @@ +package laserdisc +package protocol + +final class ClusterPSpec extends BaseSpec with ClusterP { + import clustertypes._ + import org.scalacheck.{Arbitrary, Gen} + import org.scalacheck.Gen._ + + private[this] implicit final val clusterFailoverModeArb: Arbitrary[ClusterFailoverMode] = Arbitrary { + Gen.oneOf(ClusterFailoverMode.force, ClusterFailoverMode.takeover) + } + private[this] implicit final val clusterResetModeArb: Arbitrary[ClusterResetMode] = Arbitrary { + Gen.oneOf(ClusterResetMode.hard, ClusterResetMode.soft) + } + private[this] implicit final val clusterSetSlotModeArb: Arbitrary[ClusterSetSlotMode] = Arbitrary { + Gen.oneOf(ClusterSetSlotMode.importing, ClusterSetSlotMode.migrating, ClusterSetSlotMode.node) + } + + private[this] final val kvPairPattern = ClusterP.KVPairRegex.pattern + private[this] final val infoIsValid: Map[String, String] => Boolean = _.forall { case (k, v) => kvPairPattern.matcher(s"$k:$v").matches } + private[this] final val infoGen: Gen[Map[String, String]] = nonEmptyMap(identifier.flatMap(k => alphaStr.map(k -> _))) :| "info map" + private[this] implicit final val infoShow: Show[Map[String, String]] = Show.instance { _.map { case (k, v) => s"$k:$v" }.mkString(CRLF) } + + private[this] final type RawNode = (String, String, Int, Option[Int], Seq[String], String, Int, Int, Int, String, Seq[String]) + private[this] final type RawNodes = List[RawNode] + private[this] final val rawNodeToString: RawNode => String = { + case (nid, h, p, None, fs, mnid, ps, pr, ce, l, ss) => + s"$nid $h:$p ${fs.mkString(COMMA)} $mnid $ps $pr $ce $l${ss.mkString(SPACE, SPACE, "")}" + case (nid, h, p, Some(cp), fs, mnid, ps, pr, ce, l, ss) => + s"$nid $h:$p@$cp ${fs.mkString(COMMA)} $mnid $ps $pr $ce $l${ss.mkString(SPACE, SPACE, "")}" + } + private[this] final val nodesIsValid: RawNodes => Boolean = { + val nid = NodeIdRegexWit.value + val ip = IPv4RegexWit.value + val domain = Rfc1123HostnameRegexWit.value + val flags = raw"noflags|(myself|master|slave|fail\?|fail|handshake|noaddr)(,(myself|master|slave|fail\?|fail|handshake|noaddr))*" + val link = raw"(connected|disconnected)" + val slots = raw"(\d+|(\d+-\d+)|\[\d+-[><]-$nid\])?(\s(\d+|(\d+-\d+)|\[\d+-[><]-$nid\]))*" + val R = raw"^$nid\s($ip|$domain)?:(\d+)(@\d+)?\s($flags)\s(-|$nid)\s\d+\s\d+\s\d+\s$link\s${slots}$$".r + + _.map(rawNodeToString).forall { case R(_*) => true; case _ => false } + } + private[this] final val nodesGen: Gen[RawNodes] = { + val flag = Gen.oneOf("myself", "master", "slave", "fail?", "fail", "handshake", "noaddr") + val slotType = Gen.oneOf( + slotGen.map(_.toString), + slotGen.flatMap(f => slotGen.map(t => s"$f-$t")), + slotGen.flatMap(s => nodeIdGen.map(in => s"[$s-<-$in]")), + slotGen.flatMap(s => nodeIdGen.map(en => s"[$s->-$en]")) + ) + val rawNode = for { + nid <- nodeIdGen + h <- Gen.oneOf(const(""), hostGen) + p <- portGen + mcp <- option(portGen) + fs <- Gen.oneOf(const("noflags").map(Seq(_)), choose(1, 7).flatMap(containerOfN[Set, String](_, flag).map(_.toSeq))) + mmnid <- Gen.oneOf(const("-"), nodeIdGen) + ps <- nonNegIntGen + pr <- nonNegIntGen + ce <- nonNegIntGen + l <- Gen.oneOf("connected", "disconnected") + ss <- choose(1, 20).flatMap(listOfN(_, slotType)) + } yield (nid, h, p, mcp, fs, mmnid, ps, pr, ce, l, ss) + + choose(1, 10).flatMap(listOfN(_, rawNode)) :| "raw nodes info" + } + private[this] implicit final val nodesShow: Show[RawNodes] = Show.instance { + _.map(rawNodeToString).mkString(LF) + } + private[this] final val validateNodes: (ClusterNodes, RawNodes) => Unit = (ns, rns) => + ns.nodes.zip(rns).foreach { + case (ClusterNode(n0, ClusterAddress(h0, p0, cp0), fs0, m0, ps0, pr0, ce0, l0, ss0), (n, h, p, cp, fs, m, ps, pr, ce, l, ss)) => + n0.value shouldBe n + h0.value shouldBe (if (h.isEmpty) LoopbackEqWit.value else h) + p0.value shouldBe p + cp0.value shouldBe cp.getOrElse(p) + (if (fs0.isEmpty) Seq("noflags") else fs0.map(Show[ClusterFlag].show)) shouldBe fs + m0.fold("-")(_.value) shouldBe m + ps0.value shouldBe ps + pr0.value shouldBe pr + ce0.value shouldBe ce + Show[ClusterLinkState].show(l0) shouldBe l + ss0.map { + case ClusterSingleSlotType(s) => s.toString + case ClusterRangeSlotType(f, t) => s"$f-$t" + case ClusterImportingSlotType(s, in) => s"[$s-<-$in]" + case ClusterMigratingSlotType(s, mn) => s"[$s->-$mn]" + } shouldBe ss + } + + private[this] final type RawSlot = (Int, Int, Seq[(String, Int, Option[String])]) + private[this] final type RawSlots = List[RawSlot] + private[this] final val slotsIsValid: RawSlots => Boolean = _.forall { + case (Slot(_), Slot(_), mrs) if mrs.forall { case (Host(_) | "", Port(_), Some(NodeId(_)) | None) => true; case _ => false } => true + case _ => false + } + private[this] final val slotsGen: Gen[RawSlots] = (for { + isOld <- Gen.oneOf(true, false) + rss <- choose(1, 10).flatMap { + listOfN( + _, + for { + fst <- slotGen + snd <- slotGen + mrs <- choose(1, 10).flatMap { + listOfN(_, for { + h <- Gen.oneOf(const(""), hostGen) + p <- portGen + mrid <- if (isOld) const(None) else nodeIdGen.map(Some(_)) + } yield (h, p, mrid)) + } + } yield if (fst < snd) (fst, snd, mrs) else (snd, fst, mrs) + ) + } + } yield rss) :| "raw slots info" + private[this] final val slotsToArr: RawSlots => Arr = ss => + Arr( + ss.map { + case (f, t, rs) => + Arr(Num(f.toLong), Num(t.toLong), Arr(rs.toList.map { + case (h, p, Some(mrid)) => Arr(Bulk(h), Num(p.toLong), Bulk(mrid)) + case (h, p, None) => Arr(Bulk(h), Num(p.toLong)) + })) + } + ) + + "The Cluster protocol" when { + + "using addslots" should { + + "roundtrip successfully" when { + "given one or more slots" in forAll("slots") { ss: OneOrMore[Slot] => + val protocol = addslots(ss) + + protocol.encode shouldBe Arr(Bulk("CLUSTER") :: Bulk("ADDSLOTS") :: ss.value.map(Bulk(_))) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + } + + "using clusterinfo" should { + + "roundtrip successfully" when { + "using val" in forAll(infoGen) { info => + whenever(infoIsValid(info)) { + val protocol = clusterinfo + + protocol.encode shouldBe Arr(Bulk("CLUSTER"), Bulk("INFO")) + val result = protocol.decode(Bulk(info)).right.value + info.forall { case (k, v) => result.selectDynamic(k).right.value == v } shouldBe true + } + } + } + } + + "using countfailurereports" should { + + "roundtrip successfully" when { + "given a nodeId" in forAll("node id", "failure reports") { (n: NodeId, nni: NonNegInt) => + val protocol = countfailurereports(n) + + protocol.encode shouldBe Arr(Bulk("CLUSTER"), Bulk("COUNT-FAILURE-REPORTS"), Bulk(n)) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + } + } + } + + "using countkeysinslot" should { + + "roundtrip successfully" when { + "given a slot" in forAll("slot", "keys in slot") { (s: Slot, nni: NonNegInt) => + val protocol = countkeysinslot(s) + + protocol.encode shouldBe Arr(Bulk("CLUSTER"), Bulk("COUNTKEYSINSLOT"), Bulk(s)) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + } + } + } + + "using delslots" should { + + "roundtrip successfully" when { + "given one or more slots" in forAll("slots") { ss: OneOrMore[Slot] => + val protocol = delslots(ss) + + protocol.encode shouldBe Arr(Bulk("CLUSTER") :: Bulk("DELSLOTS") :: ss.value.map(Bulk(_))) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + } + + "using failover" should { + + "roundtrip successfully" when { + "using val" in { + val protocol = failover + + protocol.encode shouldBe Arr(Bulk("CLUSTER"), Bulk("FAILOVER")) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + "given mode" in forAll("failover mode") { m: ClusterFailoverMode => + val protocol = failover(m) + + protocol.encode shouldBe Arr(Bulk("CLUSTER"), Bulk("FAILOVER"), Bulk(m)) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + } + + "using forget" should { + + "roundtrip successfully" when { + "given a nodeId" in forAll("node id") { n: NodeId => + val protocol = forget(n) + + protocol.encode shouldBe Arr(Bulk("CLUSTER"), Bulk("FORGET"), Bulk(n)) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + } + + "using getkeysinslot" should { + + "roundtrip successfully" when { + "given a slot and a count" in forAll("slot", "count", "keys in slot") { (s: Slot, pi: PosInt, ks: List[Key]) => + val protocol = getkeysinslot(s, pi) + + protocol.encode shouldBe Arr(Bulk("CLUSTER"), Bulk("GETKEYSINSLOT"), Bulk(s), Bulk(pi)) + protocol.decode(Arr(ks.map(Bulk(_)))).right.value shouldBe ks + } + } + } + + "using keyslot" should { + + "roundtrip successfully" when { + "given a key" in forAll("key", "slot") { (k: Key, s: Slot) => + val protocol = keyslot(k) + + protocol.encode shouldBe Arr(Bulk("CLUSTER"), Bulk("KEYSLOT"), Bulk(k)) + protocol.decode(Num(s.value.toLong)).right.value shouldBe s + } + } + } + + "using meet" should { + + "roundtrip successfully" when { + "given a host and a port" in forAll("host", "port") { (h: Host, p: Port) => + val protocol = meet(h, p) + + protocol.encode shouldBe Arr(Bulk("CLUSTER"), Bulk("MEET"), Bulk(h), Bulk(p)) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + } + + "using nodes" should { + + "roundtrip successfully" when { + "using val" in forAll(nodesGen) { ns => + whenever(nodesIsValid(ns)) { + val protocol = nodes + + protocol.encode shouldBe Arr(Bulk("CLUSTER"), Bulk("NODES")) + validateNodes(protocol.decode(Bulk(ns)).right.value, ns) + } + } + } + } + + "using readonly" should { + + "roundtrip successfully" when { + "using val" in { + val protocol = readonly + + protocol.encode shouldBe Arr(Bulk("CLUSTER"), Bulk("READONLY")) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + } + + "using readwrite" should { + + "roundtrip successfully" when { + "using val" in { + val protocol = readwrite + + protocol.encode shouldBe Arr(Bulk("CLUSTER"), Bulk("READWRITE")) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + } + + "using replicas" should { + + "roundtrip successfully" when { + "given a nodeId" in forAll(nodeIdArb.arbitrary, nodesGen) { (n, ns) => + whenever(nodesIsValid(ns)) { + val protocol = replicas(n) + + protocol.encode shouldBe Arr(Bulk("CLUSTER"), Bulk("REPLICAS"), Bulk(n)) + validateNodes(protocol.decode(Bulk(ns)).right.value, ns) + } + } + } + } + + "using replicate" should { + + "roundtrip successfully" when { + "given a nodeId" in forAll("node id") { n: NodeId => + val protocol = replicate(n) + + protocol.encode shouldBe Arr(Bulk("CLUSTER"), Bulk("REPLICATE"), Bulk(n)) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + } + + "using reset" should { + + "roundtrip successfully" when { + "using val" in { + val protocol = reset + + protocol.encode shouldBe Arr(Bulk("CLUSTER"), Bulk("RESET"), Bulk("SOFT")) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + "given reset mode" in forAll("reset mode") { m: ClusterResetMode => + val protocol = reset(m) + + protocol.encode shouldBe Arr(Bulk("CLUSTER"), Bulk("RESET"), Bulk(m)) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + } + + "using saveconfig" should { + + "roundtrip successfully" when { + "using val" in { + val protocol = saveconfig + + protocol.encode shouldBe Arr(Bulk("CLUSTER"), Bulk("SAVECONFIG")) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + } + + "using setconfigepoch" should { + + "roundtrip successfully" when { + "given a config epoch" in forAll("config epoch") { nni: NonNegInt => + val protocol = setconfigepoch(nni) + + protocol.encode shouldBe Arr(Bulk("CLUSTER"), Bulk("SET-CONFIG-EPOCH"), Bulk(nni)) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + } + + "using setslot" should { + + "roundtrip successfully" when { + "given a slot" in forAll("slot") { s: Slot => + val protocol = setslot(s) + + protocol.encode shouldBe Arr(Bulk("CLUSTER"), Bulk("SETSLOT"), Bulk(s), Bulk("STABLE")) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + "given a slot, mode and a node id" in forAll("slot", "mode", "node id") { (s: Slot, m: ClusterSetSlotMode, nid: NodeId) => + val protocol = setslot(s, m, nid) + + protocol.encode shouldBe Arr(Bulk("CLUSTER"), Bulk("SETSLOT"), Bulk(s), Bulk(m), Bulk(nid)) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + } + + "using slaves" should { + + "roundtrip successfully" when { + "given a nodeId" in forAll(nodeIdArb.arbitrary, nodesGen) { (n, ns) => + whenever(nodesIsValid(ns)) { + val protocol = slaves(n) + + protocol.encode shouldBe Arr(Bulk("CLUSTER"), Bulk("SLAVES"), Bulk(n)) + validateNodes(protocol.decode(Bulk(ns)).right.value, ns) + } + } + } + } + + "using slots" should { + + "roundtrip successfully" when { + "using val" in forAll(slotsGen) { ss => + whenever(slotsIsValid(ss)) { + val protocol = slots + + protocol.encode shouldBe Arr(Bulk("CLUSTER"), Bulk("SLOTS")) + protocol.decode(slotsToArr(ss)).right.value.slots.foreach { + case (ClusterRangeSlotType(f, t), ClusterNewSlotInfo(ClusterHostPortNodeId(mh, mp, mid), rs)) => + val mrs = (mh.value, mp.value, Some(mid.value)) :: rs.foldLeft(List.empty[(String, Int, Option[String])]) { + case (acc, ClusterHostPortNodeId(h, p, nid)) => acc :+ ((h.value, p.value, Some(nid.value))) + } + ss should contain((f.value, t.value, mrs)) + case (ClusterRangeSlotType(f, t), ClusterOldSlotInfo(ClusterHostPort(mh, mp), rs)) => + val mrs = (mh.value, mp.value, None) :: rs.foldLeft(List.empty[(String, Int, Option[String])]) { + case (acc, ClusterHostPort(h, p)) => acc :+ ((h.value, p.value, None)) + } + ss should contain((f.value, t.value, mrs)) + } + } + } + } + } + } +} diff --git a/core/src/test/scala/laserdisc/protocol/ConnectionPSpec.scala b/core/src/test/scala/laserdisc/protocol/ConnectionPSpec.scala new file mode 100644 index 00000000..47aedd0a --- /dev/null +++ b/core/src/test/scala/laserdisc/protocol/ConnectionPSpec.scala @@ -0,0 +1,96 @@ +package laserdisc +package protocol + +final class ConnectionPSpec extends BaseSpec with ConnectionP { + + "The Connection protocol" when { + + "using auth" should { + + "roundtrip successfully" when { + "given non empty password" in forAll("non empty password") { key: Key => + val protocol = auth(key) + + protocol.encode shouldBe Arr(Bulk("AUTH"), Bulk(key.value)) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + } + + "using echo" should { + + "roundtrip successfully" when { + "given any String message" in forAll("string") { s: String => + val protocol = echo(s) + + protocol.encode shouldBe Arr(Bulk("ECHO"), Bulk(s)) + protocol.decode(Bulk(s)).right.value shouldBe s + } + "given any Int message" in forAll("int") { i: Int => + val protocol = echo(i) + + protocol.encode shouldBe Arr(Bulk("ECHO"), Bulk(i)) + protocol.decode(Bulk(i)).right.value shouldBe i + } + } + } + + "using ping" should { + + "roundript successfully" when { + "given any String message" in forAll("string") { s: String => + val protocol = ping(s) + + protocol.encode shouldBe Arr(Bulk("PING"), Bulk(s)) + protocol.decode(Bulk(s)).right.value shouldBe s + } + "given any Int message" in forAll("int") { i: Int => + val protocol = ping(i) + + protocol.encode shouldBe Arr(Bulk("PING"), Bulk(i)) + protocol.decode(Bulk(i)).right.value shouldBe i + } + "using val to get back PONG message" in { + val protocol = ping + + protocol.encode shouldBe Arr(Bulk("PING")) + protocol.decode(Str(PONG.value)).right.value shouldBe PONG + } + } + } + + "using quit" should { + + "roundtrip successfully" in { + val protocol = quit + + protocol.encode shouldBe Arr(Bulk("QUIT")) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + + "using select" should { + + "roundtrip successfully" when { + "given valid DbIndexes" in forAll("db index") { dbi: DbIndex => + val protocol = select(dbi) + + protocol.encode shouldBe Arr(Bulk("SELECT"), Bulk(dbi)) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + } + + "using swapdb" should { + + "roundtrip successfully" when { + "given valid DbIndexes" in forAll("db index 1", "db index 2") { (dbi1: DbIndex, dbi2: DbIndex) => + val protocol = swapdb(dbi1, dbi2) + + protocol.encode shouldBe Arr(Bulk("SWAPDB"), Bulk(dbi1), Bulk(dbi2)) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + } + } +} diff --git a/core/src/test/scala/laserdisc/protocol/GeoPSpec.scala b/core/src/test/scala/laserdisc/protocol/GeoPSpec.scala new file mode 100644 index 00000000..330c28c8 --- /dev/null +++ b/core/src/test/scala/laserdisc/protocol/GeoPSpec.scala @@ -0,0 +1,857 @@ +package laserdisc +package protocol + +final class GeoPSpec extends GeoExtPSpec { + import geotypes._ + import org.scalacheck.{Arbitrary, Gen} + import org.scalacheck.Arbitrary.arbitrary + import org.scalacheck.Gen.listOf + + private[this] implicit final val geoKeyAndCoordArb: Arbitrary[GeoKeyAndCoord] = Arbitrary { + for { + k <- arbitrary[Key] + c <- arbitrary[GeoCoordinates] + } yield GeoKeyAndCoord(k, c) + } + private[this] implicit final val geoKeyAndDistArb: Arbitrary[GeoKeyAndDist] = Arbitrary { + for { + k <- arbitrary[Key] + d <- arbitrary[NonNegDouble] + } yield GeoKeyAndDist(k, d) + } + private[this] implicit final val geoKeyAndHashArb: Arbitrary[GeoKeyAndHash] = Arbitrary { + for { + k <- arbitrary[Key] + l <- arbitrary[NonNegLong] + } yield GeoKeyAndHash(k, l) + } + private[this] implicit final val geoKeyCoordAndDistArb: Arbitrary[GeoKeyCoordAndDist] = Arbitrary { + for { + k <- arbitrary[Key] + c <- arbitrary[GeoCoordinates] + d <- arbitrary[NonNegDouble] + } yield GeoKeyCoordAndDist(k, c, d) + } + private[this] implicit final val geoKeyCoordAndHashArb: Arbitrary[GeoKeyCoordAndHash] = Arbitrary { + for { + k <- arbitrary[Key] + c <- arbitrary[GeoCoordinates] + l <- arbitrary[NonNegLong] + } yield GeoKeyCoordAndHash(k, c, l) + } + private[this] implicit final val geoKeyDistAndHashArb: Arbitrary[GeoKeyDistAndHash] = Arbitrary { + for { + k <- arbitrary[Key] + d <- arbitrary[NonNegDouble] + l <- arbitrary[NonNegLong] + } yield GeoKeyDistAndHash(k, d, l) + } + private[this] implicit final val geoKeyCoordDistAndHashArb: Arbitrary[GeoKeyCoordDistAndHash] = Arbitrary { + for { + k <- arbitrary[Key] + c <- arbitrary[GeoCoordinates] + d <- arbitrary[NonNegDouble] + l <- arbitrary[NonNegLong] + } yield GeoKeyCoordDistAndHash(k, c, d, l) + } + private[this] implicit final val geoRadiusModeArb: Arbitrary[(GeoRadiusMode, List[_])] = Arbitrary { + Gen.oneOf( + listOf(geoKeyAndCoordArb.arbitrary).map(GeoRadiusMode.coordinates -> _), + listOf(geoKeyAndDistArb.arbitrary).map(GeoRadiusMode.distance -> _), + listOf(geoKeyAndHashArb.arbitrary).map(GeoRadiusMode.hash -> _), + listOf(geoKeyCoordAndDistArb.arbitrary).map(GeoRadiusMode.coordinatesAndDistance -> _), + listOf(geoKeyCoordAndHashArb.arbitrary).map(GeoRadiusMode.coordinatesAndHash -> _), + listOf(geoKeyDistAndHashArb.arbitrary).map(GeoRadiusMode.distanceAndHash -> _), + listOf(geoKeyCoordDistAndHashArb.arbitrary).map(GeoRadiusMode.all -> _) + ) + } + private[this] implicit final val geoStoreModeArb: Arbitrary[GeoStoreMode] = Arbitrary { + Gen.oneOf( + arbitrary[Key].map(GeoStoreHash(_)), + arbitrary[Key].map(GeoStoreDistance(_)), + arbitrary[Key].flatMap(hk => arbitrary[Key].map(GeoStoreBoth(hk, _))) + ) + } + + private[this] final val listToArr: List[_] => Arr = l => + Arr(l.collect { + case GeoKeyAndCoord(k, GeoCoordinates(lat, long)) => Arr(Bulk(k), Arr(Bulk(long), Bulk(lat))) + case GeoKeyAndDist(k, d) => Arr(Bulk(k), Bulk(d)) + case GeoKeyAndHash(k, h) => Arr(Bulk(k), Num(h.value)) + case GeoKeyCoordAndDist(k, GeoCoordinates(lat, long), d) => Arr(Bulk(k), Bulk(d), Arr(Bulk(long), Bulk(lat))) + case GeoKeyCoordAndHash(k, GeoCoordinates(lat, long), h) => Arr(Bulk(k), Num(h.value), Arr(Bulk(long), Bulk(lat))) + case GeoKeyDistAndHash(k, d, h) => Arr(Bulk(k), Bulk(d), Num(h.value)) + case GeoKeyCoordDistAndHash(k, GeoCoordinates(lat, long), d, h) => Arr(Bulk(k), Bulk(d), Num(h.value), Arr(Bulk(long), Bulk(lat))) + }) + + "The Geo protocol" when { + + "using geoadd" should { + + "roundtrip successfully" when { + "given key and positions" in forAll("key", "positions", "added") { (k: Key, ps: OneOrMore[GeoPosition], nni: NonNegInt) => + val protocol = geoadd(k, ps) + + protocol.encode shouldBe Arr(Bulk("GEOADD") :: Bulk(k) :: ps.value.flatMap(geoPositionToBulkList)) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + } + } + + } + + "using geodist" should { + + "roundtrip successfully" when { + "given key and members" in { + forAll("key", "member 1", "member 2", "maybe distance") { (k: Key, m1: Key, m2: Key, onnd: Option[NonNegDouble]) => + val protocol = geodist(k, m1, m2) + + protocol.encode shouldBe Arr(Bulk("GEODIST"), Bulk(k), Bulk(m1), Bulk(m2)) + protocol.decode(nonNegDoubleOptionToBulk(onnd)).right.value shouldBe onnd + } + } + "given key, members and unit" in { + forAll("key", "member 1", "member 2", "unit", "maybe distance") { + (k: Key, m1: Key, m2: Key, u: GeoUnit, onnd: Option[NonNegDouble]) => + val protocol = geodist(k, m1, m2, u) + + protocol.encode shouldBe Arr(Bulk("GEODIST"), Bulk(k), Bulk(m1), Bulk(m2), Bulk(u)) + protocol.decode(nonNegDoubleOptionToBulk(onnd)).right.value shouldBe onnd + } + } + } + } + + "using geohash" should { + + "roundtrip successfully" when { + "given key and members" in { + forAll("key", "members", "geo hashes") { (k: Key, ms: OneOrMoreKeys, oghs: OneOrMore[Option[GeoHash]]) => + val protocol = geohash(k, ms) + + protocol.encode shouldBe Arr(Bulk("GEOHASH") :: Bulk(k) :: ms.value.map(m => Bulk(m))) + protocol.decode(oneOrMoreGeoHashOptionToArr(oghs)).right.value shouldBe oghs.value + } + } + } + + } + + "using geopos" should { + + "roundtrip successfully" when { + "given key and members" in { + forAll("key", "members", "coordinates") { (k: Key, ms: OneOrMoreKeys, ocs: OneOrMore[Option[GeoCoordinates]]) => + val protocol = geopos(k, ms) + + protocol.encode shouldBe Arr(Bulk("GEOPOS") :: Bulk(k) :: ms.value.map(m => Bulk(m))) + protocol.decode(oneOrMoreGeoCoordinatesOptionToArr(ocs)).right.value shouldBe ocs.value + } + } + } + + "using georadius" should { + + "roundtrip successfully" when { + "given key, coordinates, radius and unit" in { + forAll("key", "coordinates", "radius", "unit", "members") { + (k: Key, c: GeoCoordinates, r: NonNegDouble, u: GeoUnit, ms: List[Key]) => + val protocol = georadius(k, c, r, u) + + protocol.encode shouldBe Arr(Bulk("GEORADIUS"), Bulk(k), Bulk(c.longitude), Bulk(c.latitude), Bulk(r), Bulk(u)) + protocol.decode(Arr(ms.map(Bulk(_)))).right.value shouldBe ms + } + } + "given key, coordinates, radius, unit and limit" in { + forAll("key", "coordinates", "radius", "unit", "limit", "members") { + (k: Key, c: GeoCoordinates, r: NonNegDouble, u: GeoUnit, l: PosInt, ms: List[Key]) => + val protocol = georadius(k, c, r, u, l) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUS"), + Bulk(k), + Bulk(c.longitude), + Bulk(c.latitude), + Bulk(r), + Bulk(u), + Bulk("COUNT"), + Bulk(l) + ) + protocol.decode(Arr(ms.map(Bulk(_)))).right.value shouldBe ms + } + } + "given key, coordinates, radius, unit and direction" in { + forAll("key", "coordinates", "radius", "unit", "direction", "members") { + (k: Key, c: GeoCoordinates, r: NonNegDouble, u: GeoUnit, d: Direction, ms: List[Key]) => + val protocol = georadius(k, c, r, u, d) + + protocol.encode shouldBe Arr(Bulk("GEORADIUS"), Bulk(k), Bulk(c.longitude), Bulk(c.latitude), Bulk(r), Bulk(u), Bulk(d)) + protocol.decode(Arr(ms.map(Bulk(_)))).right.value shouldBe ms + } + } + "given key, coordinates, radius, unit, limit and direction" in { + forAll("key", "coordinates", "radius", "unit", "limit", "direction") { + (k: Key, c: GeoCoordinates, r: NonNegDouble, u: GeoUnit, l: PosInt, d: Direction) => + forAll("members") { ms: List[Key] => + val protocol = georadius(k, c, r, u, l, d) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUS"), + Bulk(k), + Bulk(c.longitude), + Bulk(c.latitude), + Bulk(r), + Bulk(u), + Bulk("COUNT"), + Bulk(l), + Bulk(d) + ) + protocol.decode(Arr(ms.map(Bulk(_)))).right.value shouldBe ms + } + } + } + "given key, coordinates, radius, unit and radius mode" in { + forAll("key", "coordinates", "radius", "unit", "radius mode & result") { + (k: Key, c: GeoCoordinates, r: NonNegDouble, u: GeoUnit, rmAndRes: (GeoRadiusMode, List[_])) => + val (rm, res) = rmAndRes + implicit val _ = rm.r + val protocol = georadius(k, c, r, u, rm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUS") :: + Bulk(k) :: + Bulk(c.longitude) :: + Bulk(c.latitude) :: + Bulk(r) :: + Bulk(u) :: + rm.params.map(Bulk(_)) + ) + protocol.decode(listToArr(res)).right.value shouldBe res + } + } + "given key, coordinates, radius, unit, limit and radius mode" in { + forAll("key", "coordinates", "radius", "unit", "limit", "radius mode & result") { + (k: Key, c: GeoCoordinates, r: NonNegDouble, u: GeoUnit, l: PosInt, rmAndRes: (GeoRadiusMode, List[_])) => + val (rm, res) = rmAndRes + implicit val _ = rm.r + val protocol = georadius(k, c, r, u, l, rm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUS") :: + Bulk(k) :: + Bulk(c.longitude) :: + Bulk(c.latitude) :: + Bulk(r) :: + Bulk(u) :: + Bulk("COUNT") :: + Bulk(l) :: + rm.params.map(Bulk(_)) + ) + protocol.decode(listToArr(res)).right.value shouldBe res + } + } + "given key, coordinates, radius, unit, direction and radius mode" in { + forAll("key", "coordinates", "radius", "unit", "direction", "radius mode & result") { + (k: Key, c: GeoCoordinates, r: NonNegDouble, u: GeoUnit, d: Direction, rmAndRes: (GeoRadiusMode, List[_])) => + val (rm, res) = rmAndRes + implicit val _ = rm.r + val protocol = georadius(k, c, r, u, d, rm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUS") :: + Bulk(k) :: + Bulk(c.longitude) :: + Bulk(c.latitude) :: + Bulk(r) :: + Bulk(u) :: + Bulk(d) :: + rm.params.map(Bulk(_)) + ) + protocol.decode(listToArr(res)).right.value shouldBe res + } + } + "given key, coordinates, radius, unit, limit, direction and radius mode" in { + forAll("key", "coordinates", "radius", "unit", "limit", "direction") { + (k: Key, c: GeoCoordinates, r: NonNegDouble, u: GeoUnit, l: PosInt, d: Direction) => + forAll("radius mode & result") { rmAndRes: (GeoRadiusMode, List[_]) => + val (rm, res) = rmAndRes + implicit val _ = rm.r + val protocol = georadius(k, c, r, u, l, d, rm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUS") :: + Bulk(k) :: + Bulk(c.longitude) :: + Bulk(c.latitude) :: + Bulk(r) :: + Bulk(u) :: + Bulk("COUNT") :: + Bulk(l) :: + Bulk(d) :: + rm.params.map(Bulk(_)) + ) + protocol.decode(listToArr(res)).right.value shouldBe res + } + } + } + "given key, coordinates, radius, unit and store mode" in { + forAll("key", "coordinates", "radius", "unit", "store mode", "stored") { + (k: Key, c: GeoCoordinates, r: NonNegDouble, u: GeoUnit, sm: GeoStoreMode, nni: NonNegInt) => + val protocol = georadius(k, c, r, u, sm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUS") :: Bulk(k) :: Bulk(c.longitude) :: Bulk(c.latitude) :: Bulk(r) :: Bulk(u) :: sm.params.map(Bulk(_)) + ) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + } + } + "given key, coordinates, radius, unit, limit and store mode" in { + forAll("key", "coordinates", "radius", "unit", "limit", "store mode") { + (k: Key, c: GeoCoordinates, r: NonNegDouble, u: GeoUnit, l: PosInt, sm: GeoStoreMode) => + forAll("stored") { nni: NonNegInt => + val protocol = georadius(k, c, r, u, l, sm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUS") :: + Bulk(k) :: + Bulk(c.longitude) :: + Bulk(c.latitude) :: + Bulk(r) :: + Bulk(u) :: + Bulk("COUNT") :: + Bulk(l) :: + sm.params.map(Bulk(_)) + ) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + } + } + } + "given key, coordinates, radius, unit, direction and store mode" in { + forAll("key", "coordinates", "radius", "unit", "direction", "store mode") { + (k: Key, c: GeoCoordinates, r: NonNegDouble, u: GeoUnit, d: Direction, sm: GeoStoreMode) => + forAll("stored") { nni: NonNegInt => + val protocol = georadius(k, c, r, u, d, sm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUS") :: + Bulk(k) :: + Bulk(c.longitude) :: + Bulk(c.latitude) :: + Bulk(r) :: + Bulk(u) :: + Bulk(d) :: + sm.params.map(Bulk(_)) + ) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + } + } + } + "given key, coordinates, radius, unit, limit, direction and store mode" in { + forAll("key", "coordinates", "radius", "unit", "limit", "direction") { + (k: Key, c: GeoCoordinates, r: NonNegDouble, u: GeoUnit, l: PosInt, d: Direction) => + forAll("store mode", "stored") { (sm: GeoStoreMode, nni: NonNegInt) => + val protocol = georadius(k, c, r, u, l, d, sm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUS") :: + Bulk(k) :: + Bulk(c.longitude) :: + Bulk(c.latitude) :: + Bulk(r) :: + Bulk(u) :: + Bulk("COUNT") :: + Bulk(l) :: + Bulk(d) :: + sm.params.map(Bulk(_)) + ) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + } + } + } + + "given key, member, radius and unit" in { + forAll("key", "member", "radius", "unit", "members") { (k: Key, m: Key, r: NonNegDouble, u: GeoUnit, ms: List[Key]) => + val protocol = georadius(k, m, r, u) + + protocol.encode shouldBe Arr(Bulk("GEORADIUSBYMEMBER"), Bulk(k), Bulk(m), Bulk(r), Bulk(u)) + protocol.decode(Arr(ms.map(Bulk(_)))).right.value shouldBe ms + } + } + "given key, member, radius, unit and limit" in { + forAll("key", "coordinates", "radius", "unit", "limit", "members") { + (k: Key, m: Key, r: NonNegDouble, u: GeoUnit, l: PosInt, ms: List[Key]) => + val protocol = georadius(k, m, r, u, l) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUSBYMEMBER"), + Bulk(k), + Bulk(m), + Bulk(r), + Bulk(u), + Bulk("COUNT"), + Bulk(l) + ) + protocol.decode(Arr(ms.map(Bulk(_)))).right.value shouldBe ms + } + } + "given key, member, radius, unit and direction" in { + forAll("key", "member", "radius", "unit", "direction", "members") { + (k: Key, m: Key, r: NonNegDouble, u: GeoUnit, d: Direction, ms: List[Key]) => + val protocol = georadius(k, m, r, u, d) + + protocol.encode shouldBe Arr(Bulk("GEORADIUSBYMEMBER"), Bulk(k), Bulk(m), Bulk(r), Bulk(u), Bulk(d)) + protocol.decode(Arr(ms.map(Bulk(_)))).right.value shouldBe ms + } + } + "given key, member, radius, unit, limit and direction" in { + forAll("key", "member", "radius", "unit", "limit", "direction") { + (k: Key, m: Key, r: NonNegDouble, u: GeoUnit, l: PosInt, d: Direction) => + forAll("members") { ms: List[Key] => + val protocol = georadius(k, m, r, u, l, d) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUSBYMEMBER"), + Bulk(k), + Bulk(m), + Bulk(r), + Bulk(u), + Bulk("COUNT"), + Bulk(l), + Bulk(d) + ) + protocol.decode(Arr(ms.map(Bulk(_)))).right.value shouldBe ms + } + } + } + "given key, member, radius, unit and radius mode" in { + forAll("key", "member", "radius", "unit", "radius mode & result") { + (k: Key, m: Key, r: NonNegDouble, u: GeoUnit, rmAndRes: (GeoRadiusMode, List[_])) => + val (rm, res) = rmAndRes + implicit val _ = rm.r + val protocol = georadius(k, m, r, u, rm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUSBYMEMBER") :: + Bulk(k) :: + Bulk(m) :: + Bulk(r) :: + Bulk(u) :: + rm.params.map(Bulk(_)) + ) + protocol.decode(listToArr(res)).right.value shouldBe res + } + } + "given key, member, radius, unit, limit and radius mode" in { + forAll("key", "member", "radius", "unit", "limit", "radius mode & result") { + (k: Key, m: Key, r: NonNegDouble, u: GeoUnit, l: PosInt, rmAndRes: (GeoRadiusMode, List[_])) => + val (rm, res) = rmAndRes + implicit val _ = rm.r + val protocol = georadius(k, m, r, u, l, rm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUSBYMEMBER") :: + Bulk(k) :: + Bulk(m) :: + Bulk(r) :: + Bulk(u) :: + Bulk("COUNT") :: + Bulk(l) :: + rm.params.map(Bulk(_)) + ) + protocol.decode(listToArr(res)).right.value shouldBe res + } + } + "given key, member, radius, unit, direction and radius mode" in { + forAll("key", "member", "radius", "unit", "direction", "radius mode & result") { + (k: Key, m: Key, r: NonNegDouble, u: GeoUnit, d: Direction, rmAndRes: (GeoRadiusMode, List[_])) => + val (rm, res) = rmAndRes + implicit val _ = rm.r + val protocol = georadius(k, m, r, u, d, rm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUSBYMEMBER") :: + Bulk(k) :: + Bulk(m) :: + Bulk(r) :: + Bulk(u) :: + Bulk(d) :: + rm.params.map(Bulk(_)) + ) + protocol.decode(listToArr(res)).right.value shouldBe res + } + } + "given key, member, radius, unit, limit, direction and radius mode" in { + forAll("key", "member", "radius", "unit", "limit", "direction") { + (k: Key, m: Key, r: NonNegDouble, u: GeoUnit, l: PosInt, d: Direction) => + forAll("radius mode & result") { rmAndRes: (GeoRadiusMode, List[_]) => + val (rm, res) = rmAndRes + implicit val _ = rm.r + val protocol = georadius(k, m, r, u, l, d, rm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUSBYMEMBER") :: + Bulk(k) :: + Bulk(m) :: + Bulk(r) :: + Bulk(u) :: + Bulk("COUNT") :: + Bulk(l) :: + Bulk(d) :: + rm.params.map(Bulk(_)) + ) + protocol.decode(listToArr(res)).right.value shouldBe res + } + } + } + "given key, member, radius, unit and store mode" in { + forAll("key", "member", "radius", "unit", "store mode", "stored") { + (k: Key, m: Key, r: NonNegDouble, u: GeoUnit, sm: GeoStoreMode, nni: NonNegInt) => + val protocol = georadius(k, m, r, u, sm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUSBYMEMBER") :: Bulk(k) :: Bulk(m) :: Bulk(r) :: Bulk(u) :: sm.params.map(Bulk(_)) + ) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + } + } + "given key, member, radius, unit, limit and store mode" in { + forAll("key", "member", "radius", "unit", "limit", "store mode") { + (k: Key, m: Key, r: NonNegDouble, u: GeoUnit, l: PosInt, sm: GeoStoreMode) => + forAll("stored") { nni: NonNegInt => + val protocol = georadius(k, m, r, u, l, sm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUSBYMEMBER") :: + Bulk(k) :: + Bulk(m) :: + Bulk(r) :: + Bulk(u) :: + Bulk("COUNT") :: + Bulk(l) :: + sm.params.map(Bulk(_)) + ) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + } + } + } + "given key, member, radius, unit, direction and store mode" in { + forAll("key", "member", "radius", "unit", "direction", "store mode") { + (k: Key, m: Key, r: NonNegDouble, u: GeoUnit, d: Direction, sm: GeoStoreMode) => + forAll("stored") { nni: NonNegInt => + val protocol = georadius(k, m, r, u, d, sm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUSBYMEMBER") :: Bulk(k) :: Bulk(m) :: Bulk(r) :: Bulk(u) :: Bulk(d) :: sm.params.map(Bulk(_)) + ) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + } + } + } + "given key, member, radius, unit, limit, direction and store mode" in { + forAll("key", "member", "radius", "unit", "limit", "direction") { + (k: Key, m: Key, r: NonNegDouble, u: GeoUnit, l: PosInt, d: Direction) => + forAll("store mode", "stored") { (sm: GeoStoreMode, nni: NonNegInt) => + val protocol = georadius(k, m, r, u, l, d, sm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUSBYMEMBER") :: + Bulk(k) :: + Bulk(m) :: + Bulk(r) :: + Bulk(u) :: + Bulk("COUNT") :: + Bulk(l) :: + Bulk(d) :: + sm.params.map(Bulk(_)) + ) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + } + } + } + } + } + + "using ro.georadius" should { + + "roundtrip successfully" when { + "given key, coordinates, radius and unit" in { + forAll("key", "coordinates", "radius", "unit", "members") { + (k: Key, c: GeoCoordinates, r: NonNegDouble, u: GeoUnit, ms: List[Key]) => + val protocol = ro.georadius(k, c, r, u) + + protocol.encode shouldBe Arr(Bulk("GEORADIUS_RO"), Bulk(k), Bulk(c.longitude), Bulk(c.latitude), Bulk(r), Bulk(u)) + protocol.decode(Arr(ms.map(Bulk(_)))).right.value shouldBe ms + } + } + "given key, coordinates, radius, unit and limit" in { + forAll("key", "coordinates", "radius", "unit", "limit", "members") { + (k: Key, c: GeoCoordinates, r: NonNegDouble, u: GeoUnit, l: PosInt, ms: List[Key]) => + val protocol = ro.georadius(k, c, r, u, l) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUS_RO"), + Bulk(k), + Bulk(c.longitude), + Bulk(c.latitude), + Bulk(r), + Bulk(u), + Bulk("COUNT"), + Bulk(l) + ) + protocol.decode(Arr(ms.map(Bulk(_)))).right.value shouldBe ms + } + } + "given key, coordinates, radius, unit and direction" in { + forAll("key", "coordinates", "radius", "unit", "direction", "members") { + (k: Key, c: GeoCoordinates, r: NonNegDouble, u: GeoUnit, d: Direction, ms: List[Key]) => + val protocol = ro.georadius(k, c, r, u, d) + + protocol.encode shouldBe Arr(Bulk("GEORADIUS_RO"), Bulk(k), Bulk(c.longitude), Bulk(c.latitude), Bulk(r), Bulk(u), Bulk(d)) + protocol.decode(Arr(ms.map(Bulk(_)))).right.value shouldBe ms + } + } + "given key, coordinates, radius, unit, limit and direction" in { + forAll("key", "coordinates", "radius", "unit", "limit", "direction") { + (k: Key, c: GeoCoordinates, r: NonNegDouble, u: GeoUnit, l: PosInt, d: Direction) => + forAll("members") { ms: List[Key] => + val protocol = ro.georadius(k, c, r, u, l, d) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUS_RO"), + Bulk(k), + Bulk(c.longitude), + Bulk(c.latitude), + Bulk(r), + Bulk(u), + Bulk("COUNT"), + Bulk(l), + Bulk(d) + ) + protocol.decode(Arr(ms.map(Bulk(_)))).right.value shouldBe ms + } + } + } + "given key, coordinates, radius, unit and radius mode" in { + forAll("key", "coordinates", "radius", "unit", "radius mode & result") { + (k: Key, c: GeoCoordinates, r: NonNegDouble, u: GeoUnit, rmAndRes: (GeoRadiusMode, List[_])) => + val (rm, res) = rmAndRes + implicit val _ = rm.r + val protocol = ro.georadius(k, c, r, u, rm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUS_RO") :: + Bulk(k) :: + Bulk(c.longitude) :: + Bulk(c.latitude) :: + Bulk(r) :: + Bulk(u) :: + rm.params.map(Bulk(_)) + ) + protocol.decode(listToArr(res)).right.value shouldBe res + } + } + "given key, coordinates, radius, unit, limit and radius mode" in { + forAll("key", "coordinates", "radius", "unit", "limit", "radius mode & result") { + (k: Key, c: GeoCoordinates, r: NonNegDouble, u: GeoUnit, l: PosInt, rmAndRes: (GeoRadiusMode, List[_])) => + val (rm, res) = rmAndRes + implicit val _ = rm.r + val protocol = ro.georadius(k, c, r, u, l, rm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUS_RO") :: + Bulk(k) :: + Bulk(c.longitude) :: + Bulk(c.latitude) :: + Bulk(r) :: + Bulk(u) :: + Bulk("COUNT") :: + Bulk(l) :: + rm.params.map(Bulk(_)) + ) + protocol.decode(listToArr(res)).right.value shouldBe res + } + } + "given key, coordinates, radius, unit, direction and radius mode" in { + forAll("key", "coordinates", "radius", "unit", "direction", "radius mode & result") { + (k: Key, c: GeoCoordinates, r: NonNegDouble, u: GeoUnit, d: Direction, rmAndRes: (GeoRadiusMode, List[_])) => + val (rm, res) = rmAndRes + implicit val _ = rm.r + val protocol = ro.georadius(k, c, r, u, d, rm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUS_RO") :: + Bulk(k) :: + Bulk(c.longitude) :: + Bulk(c.latitude) :: + Bulk(r) :: + Bulk(u) :: + Bulk(d) :: + rm.params.map(Bulk(_)) + ) + protocol.decode(listToArr(res)).right.value shouldBe res + } + } + "given key, coordinates, radius, unit, limit, direction and radius mode" in { + forAll("key", "coordinates", "radius", "unit", "limit", "direction") { + (k: Key, c: GeoCoordinates, r: NonNegDouble, u: GeoUnit, l: PosInt, d: Direction) => + forAll("radius mode & result") { rmAndRes: (GeoRadiusMode, List[_]) => + val (rm, res) = rmAndRes + implicit val _ = rm.r + val protocol = ro.georadius(k, c, r, u, l, d, rm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUS_RO") :: + Bulk(k) :: + Bulk(c.longitude) :: + Bulk(c.latitude) :: + Bulk(r) :: + Bulk(u) :: + Bulk("COUNT") :: + Bulk(l) :: + Bulk(d) :: + rm.params.map(Bulk(_)) + ) + protocol.decode(listToArr(res)).right.value shouldBe res + } + } + } + + "given key, member, radius and unit" in { + forAll("key", "member", "radius", "unit", "members") { (k: Key, m: Key, r: NonNegDouble, u: GeoUnit, ms: List[Key]) => + val protocol = ro.georadius(k, m, r, u) + + protocol.encode shouldBe Arr(Bulk("GEORADIUSBYMEMBER_RO"), Bulk(k), Bulk(m), Bulk(r), Bulk(u)) + protocol.decode(Arr(ms.map(Bulk(_)))).right.value shouldBe ms + } + } + "given key, member, radius, unit and limit" in { + forAll("key", "coordinates", "radius", "unit", "limit", "members") { + (k: Key, m: Key, r: NonNegDouble, u: GeoUnit, l: PosInt, ms: List[Key]) => + val protocol = ro.georadius(k, m, r, u, l) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUSBYMEMBER_RO"), + Bulk(k), + Bulk(m), + Bulk(r), + Bulk(u), + Bulk("COUNT"), + Bulk(l) + ) + protocol.decode(Arr(ms.map(Bulk(_)))).right.value shouldBe ms + } + } + "given key, member, radius, unit and direction" in { + forAll("key", "member", "radius", "unit", "direction", "members") { + (k: Key, m: Key, r: NonNegDouble, u: GeoUnit, d: Direction, ms: List[Key]) => + val protocol = ro.georadius(k, m, r, u, d) + + protocol.encode shouldBe Arr(Bulk("GEORADIUSBYMEMBER_RO"), Bulk(k), Bulk(m), Bulk(r), Bulk(u), Bulk(d)) + protocol.decode(Arr(ms.map(Bulk(_)))).right.value shouldBe ms + } + } + "given key, member, radius, unit, limit and direction" in { + forAll("key", "member", "radius", "unit", "limit", "direction") { + (k: Key, m: Key, r: NonNegDouble, u: GeoUnit, l: PosInt, d: Direction) => + forAll("members") { ms: List[Key] => + val protocol = ro.georadius(k, m, r, u, l, d) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUSBYMEMBER_RO"), + Bulk(k), + Bulk(m), + Bulk(r), + Bulk(u), + Bulk("COUNT"), + Bulk(l), + Bulk(d) + ) + protocol.decode(Arr(ms.map(Bulk(_)))).right.value shouldBe ms + } + } + } + "given key, member, radius, unit and radius mode" in { + forAll("key", "member", "radius", "unit", "radius mode & result") { + (k: Key, m: Key, r: NonNegDouble, u: GeoUnit, rmAndRes: (GeoRadiusMode, List[_])) => + val (rm, res) = rmAndRes + implicit val _ = rm.r + val protocol = ro.georadius(k, m, r, u, rm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUSBYMEMBER_RO") :: + Bulk(k) :: + Bulk(m) :: + Bulk(r) :: + Bulk(u) :: + rm.params.map(Bulk(_)) + ) + protocol.decode(listToArr(res)).right.value shouldBe res + } + } + "given key, member, radius, unit, limit and radius mode" in { + forAll("key", "member", "radius", "unit", "limit", "radius mode & result") { + (k: Key, m: Key, r: NonNegDouble, u: GeoUnit, l: PosInt, rmAndRes: (GeoRadiusMode, List[_])) => + val (rm, res) = rmAndRes + implicit val _ = rm.r + val protocol = ro.georadius(k, m, r, u, l, rm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUSBYMEMBER_RO") :: + Bulk(k) :: + Bulk(m) :: + Bulk(r) :: + Bulk(u) :: + Bulk("COUNT") :: + Bulk(l) :: + rm.params.map(Bulk(_)) + ) + protocol.decode(listToArr(res)).right.value shouldBe res + } + } + "given key, member, radius, unit, direction and radius mode" in { + forAll("key", "member", "radius", "unit", "direction", "radius mode & result") { + (k: Key, m: Key, r: NonNegDouble, u: GeoUnit, d: Direction, rmAndRes: (GeoRadiusMode, List[_])) => + val (rm, res) = rmAndRes + implicit val _ = rm.r + val protocol = ro.georadius(k, m, r, u, d, rm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUSBYMEMBER_RO") :: + Bulk(k) :: + Bulk(m) :: + Bulk(r) :: + Bulk(u) :: + Bulk(d) :: + rm.params.map(Bulk(_)) + ) + protocol.decode(listToArr(res)).right.value shouldBe res + } + } + "given key, member, radius, unit, limit, direction and radius mode" in { + forAll("key", "member", "radius", "unit", "limit", "direction") { + (k: Key, m: Key, r: NonNegDouble, u: GeoUnit, l: PosInt, d: Direction) => + forAll("radius mode & result") { rmAndRes: (GeoRadiusMode, List[_]) => + val (rm, res) = rmAndRes + implicit val _ = rm.r + val protocol = ro.georadius(k, m, r, u, l, d, rm) + + protocol.encode shouldBe Arr( + Bulk("GEORADIUSBYMEMBER_RO") :: + Bulk(k) :: + Bulk(m) :: + Bulk(r) :: + Bulk(u) :: + Bulk("COUNT") :: + Bulk(l) :: + Bulk(d) :: + rm.params.map(Bulk(_)) + ) + protocol.decode(listToArr(res)).right.value shouldBe res + } + } + } + } + } + } + } +} diff --git a/core/src/test/scala/laserdisc/protocol/HashPSpec.scala b/core/src/test/scala/laserdisc/protocol/HashPSpec.scala new file mode 100644 index 00000000..aaf563cc --- /dev/null +++ b/core/src/test/scala/laserdisc/protocol/HashPSpec.scala @@ -0,0 +1,277 @@ +package laserdisc +package protocol + +final class HashPSpec extends HashExtPSpec { + import shapeless._ + + private[this] final val scanKVToArr: ScanKV => Arr = scanKV => + Arr( + Bulk(scanKV.cursor.value), + scanKV.maybeValues.fold(NilArr: GenArr)(kvs => Arr(kvs.flatMap { case KV(k, v) => List(Bulk(k.value), Bulk(v)) }.toList)) + ) + + "The Hash protocol" when { + + "using hdel" should { + + "roundtrip successfully" when { + "given key and fields" in forAll { (k: Key, fs: OneOrMoreKeys, nni: NonNegInt) => + val protocol = hdel(k, fs) + + protocol.encode shouldBe Arr(Bulk("HDEL") :: Bulk(k) :: fs.value.map(Bulk(_))) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + } + } + } + + "using hexists" should { + + "roundtrip successfully" when { + "given key and field" in forAll { (k: Key, f: Key, b: Boolean) => + val protocol = hexists(k, f) + + protocol.encode shouldBe Arr(Bulk("HEXISTS"), Bulk(k), Bulk(f)) + protocol.decode(boolToNum(b)).right.value shouldBe b + } + } + + } + + "using hget" should { + + "fail to compile" when { + "given key and field but missing read instance" in { + """hget[Bar](Key("a"), Key("f"))""" shouldNot compile + } + } + + "roundtrip successfully" when { + "given key and field" in forAll("key", "field", "returned value") { (k: Key, f: Key, s: String) => + val protocol = hget(k, f) + + protocol.encode shouldBe Arr(Bulk("HGET"), Bulk(k), Bulk(f)) + protocol.decode(Bulk(s)).right.value shouldBe Some(Bulk(s)) + } + "given key, field and specific read instance" in forAll("key", "field", "returned value") { (k: Key, f: Key, i: Int) => + val protocol = hget[Foo](k, f) + + protocol.encode shouldBe Arr(Bulk("HGET"), Bulk(k), Bulk(f)) + protocol.decode(Bulk(i)).right.value shouldBe Some(Foo(i)) + } + } + } + + "using hgetall" should { + + "fail to compile" when { + "given key but missing read instance" in { + """hgetall[Map[String, Int]](Key("a"))""" shouldNot compile + } + } + + "roundtrip successfully" when { + "given key" in forAll("key", "returned field", "returned value") { (k: Key, f: Key, v: String) => + val protocol = hgetall(k) + + protocol.encode shouldBe Arr(Bulk("HGETALL"), Bulk(k)) + protocol.decode(Arr(Bulk(f), Bulk(v))).right.value shouldBe Arr(Bulk(f), Bulk(v)) + } + "given key and specific read instance (Map[Key, String])" in { + forAll("key", "returned field", "returned value") { (k: Key, f: Key, v: String) => + val protocol = hgetall[Map[Key, String]](k) + + protocol.encode shouldBe Arr(Bulk("HGETALL"), Bulk(k)) + protocol.decode(Arr(Bulk(f), Bulk(v))).right.value shouldBe Map(f -> v) + } + } + "given key and specific read instance (Key :: String :: HNil)" in { + forAll("key", "returned field", "returned value") { (k: Key, f: Key, v: String) => + val protocol = hgetall[Key :: String :: HNil](k) + + protocol.encode shouldBe Arr(Bulk("HGETALL"), Bulk(k)) + protocol.decode(Arr(Bulk(f), Bulk(v))).right.value shouldBe f :: v :: HNil + } + } + } + } + + "using hincrby" should { + + "roundtrip successfully" when { + "given key, field and long increment" in { + forAll("key", "field", "increment", "incremented value") { (k: Key, f: Key, nzl: NonZeroLong, l: Long) => + val protocol = hincrby(k, f, nzl) + + protocol.encode shouldBe Arr(Bulk("HINCRBY"), Bulk(k), Bulk(f), Bulk(nzl)) + protocol.decode(Num(l)).right.value shouldBe l + } + } + "given key, field and double increment" in { + forAll("key", "field", "increment", "incremented value") { (k: Key, f: Key, nzd: NonZeroDouble, d: Double) => + val protocol = hincrby(k, f, nzd) + + protocol.encode shouldBe Arr(Bulk("HINCRBYFLOAT"), Bulk(k), Bulk(f), Bulk(nzd)) + protocol.decode(Bulk(d)).right.value shouldBe d + } + } + } + } + + "using hkeys" should { + + "roundtrip successfully" when { + "given key" in forAll("key", "returned keys") { (k: Key, ks: List[Key]) => + whenever(keyIsValid(k.value) && ks.forall(k => keyIsValid(k.value))) { + val protocol = hkeys(k) + + protocol.encode shouldBe Arr(Bulk("HKEYS"), Bulk(k)) + protocol.decode(Arr(ks.map(Bulk(_)))).right.value shouldBe ks + } + } + } + } + + "using hlen" should { + + "roundtrip successfully" when { + "given key" in forAll("key", "length") { (k: Key, nni: NonNegInt) => + val protocol = hlen(k) + + protocol.encode shouldBe Arr(Bulk("HLEN"), Bulk(k)) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + } + } + } + + "using hmget" should { + + "roundtrip successfully" when { + "given key and one field" in forAll { (k: Key, f: Key, i: Int) => + val protocol = hmget[Int](k, f) + + protocol.encode shouldBe Arr(Bulk("HMGET"), Bulk(k), Bulk(f)) + protocol.decode(Arr(Bulk(i))).right.value shouldBe i + } + "given key and two fields" in forAll { (k: Key, f1: Key, f2: Key, i: Int, s: String) => + val protocol = hmget[Int, String](k, f1, f2) + + protocol.encode shouldBe Arr(Bulk("HMGET"), Bulk(k), Bulk(f1), Bulk(f2)) + protocol.decode(Arr(Bulk(i), Bulk(s))).right.value shouldBe (i -> s) + } + } + + "using hmset" should { + + "fail to compile" when { + "given key and HNil" in { + """hmset(Key("a"), HNil)""" shouldNot compile + } + } + + "roundtrip successfully" when { + "given key and HList of (Key, A) pairs" in forAll { (k: Key, f1: Key, i: Int, f2: Key, s: String) => + val protocol = hmset(k, (f1 -> i) :: (f2 -> s) :: HNil) + + protocol.encode shouldBe Arr(Bulk("HMSET"), Bulk(k), Bulk(f1), Bulk(i), Bulk(f2), Bulk(s)) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + "given key and a Product (Baz)" in forAll { (k: Key, baz: Baz) => + val protocol = hmset(k, baz) + + protocol.encode shouldBe Arr(Bulk("HMSET"), Bulk(k), Bulk("f1"), Bulk(baz.f1), Bulk("f2"), Bulk(baz.f2)) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + } + } + + "using hscan" should { + + "roundtrip successfully" when { + "given key and cursor" in forAll("key", "cursor", "scan result") { (k: Key, nnl: NonNegLong, skv: ScanKV) => + val protocol = hscan(k, nnl) + + protocol.encode shouldBe Arr(Bulk("HSCAN"), Bulk(k), Bulk(nnl)) + protocol.decode(scanKVToArr(skv)).right.value shouldBe skv + } + "given key, cursor and glob pattern" in { + forAll("key", "cursor", "glob pattern", "scan result") { (k: Key, nnl: NonNegLong, g: GlobPattern, skv: ScanKV) => + val protocol = hscan(k, nnl, g) + + protocol.encode shouldBe Arr(Bulk("HSCAN"), Bulk(k), Bulk(nnl), Bulk("MATCH"), Bulk(g)) + protocol.decode(scanKVToArr(skv)).right.value shouldBe skv + } + } + "given key, cursor and count" in { + forAll("key", "cursor", "count", "scan result") { (k: Key, nnl: NonNegLong, pi: PosInt, skv: ScanKV) => + val protocol = hscan(k, nnl, pi) + + protocol.encode shouldBe Arr(Bulk("HSCAN"), Bulk(k), Bulk(nnl), Bulk("COUNT"), Bulk(pi)) + protocol.decode(scanKVToArr(skv)).right.value shouldBe skv + } + } + "given key, cursor, glob pattern and count" in forAll("key", "cursor", "glob pattern", "count", "scan result") { + (k: Key, nnl: NonNegLong, g: GlobPattern, pi: PosInt, skv: ScanKV) => + val protocol = hscan(k, nnl, g, pi) + + protocol.encode shouldBe Arr(Bulk("HSCAN"), Bulk(k), Bulk(nnl), Bulk("MATCH"), Bulk(g), Bulk("COUNT"), Bulk(pi)) + protocol.decode(scanKVToArr(skv)).right.value shouldBe skv + } + } + } + + "using hset" should { + + "roundtrip successfully" when { + "given key, field and value" in forAll("key", "field", "value", "success") { (k: Key, f: Key, v: Int, b: Boolean) => + val protocol = hset(k, f, v) + + protocol.encode shouldBe Arr(Bulk("HSET"), Bulk(k), Bulk(f), Bulk(v)) + protocol.decode(boolToNum(b)).right.value shouldBe b + } + } + } + + "using hsetnx" should { + + "roundtrip successfully" when { + "given key, field and value" in forAll("key", "field", "value", "success") { (k: Key, f: Key, v: Int, b: Boolean) => + val protocol = hsetnx(k, f, v) + + protocol.encode shouldBe Arr(Bulk("HSETNX"), Bulk(k), Bulk(f), Bulk(v)) + protocol.decode(boolToNum(b)).right.value shouldBe b + } + } + } + + "using hstrlen" should { + + "roundtrip successfully" when { + "given key and field" in forAll("key", "field", "length") { (k: Key, f: Key, nni: NonNegInt) => + val protocol = hstrlen(k, f) + + protocol.encode shouldBe Arr(Bulk("HSTRLEN"), Bulk(k), Bulk(f)) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + } + } + } + + "using hvals" should { + + "roundtrip successfully" when { + "given key (expecting one field)" in forAll { (k: Key, i: Int) => + val protocol = hvals[Int :: HNil](k) + + protocol.encode shouldBe Arr(Bulk("HVALS"), Bulk(k)) + protocol.decode(Arr(Bulk(i))).right.value shouldBe (i :: HNil) + } + "given key (expecting two fields)" in forAll { (k: Key, i: Int, s: String) => + val protocol = hvals[Int :: String :: HNil](k) + + protocol.encode shouldBe Arr(Bulk("HVALS"), Bulk(k)) + protocol.decode(Arr(Bulk(i), Bulk(s))).right.value shouldBe (i :: s :: HNil) + } + } + } + } +} diff --git a/core/src/test/scala/laserdisc/protocol/HyperLogLogPSpec.scala b/core/src/test/scala/laserdisc/protocol/HyperLogLogPSpec.scala new file mode 100644 index 00000000..8b843925 --- /dev/null +++ b/core/src/test/scala/laserdisc/protocol/HyperLogLogPSpec.scala @@ -0,0 +1,44 @@ +package laserdisc +package protocol + +final class HyperLogLogPSpec extends HyperLogLogExtPSpec { + + "The HyperLogLog protocol" when { + + "using pfadd" should { + + "roundtrip successfully" when { + "given key and elements" in forAll("key", "elements", "added") { (k: Key, es: OneOrMoreKeys, b: Boolean) => + val protocol = pfadd(k, es) + + protocol.encode shouldBe Arr(Bulk("PFADD") :: Bulk(k) :: es.value.map(Bulk(_))) + protocol.decode(boolToNum(b)).right.value shouldBe b + } + } + } + + "using pfcount" should { + + "roundtrip successfully" when { + "given keys" in forAll("keys", "count") { (ks: OneOrMoreKeys, nni: NonNegInt) => + val protocol = pfcount(ks) + + protocol.encode shouldBe Arr(Bulk("PFCOUNT") :: ks.value.map(Bulk(_))) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + } + } + } + + "using pfmerge" should { + + "roundtrip successfully" when { + "given two or more source keys and a destination key" in forAll("source keys", "destination key") { (sks: TwoOrMoreKeys, dk: Key) => + val protocol = pfmerge(sks, dk) + + protocol.encode shouldBe Arr(Bulk("PFMERGE") :: Bulk(dk) :: sks.value.map(Bulk(_))) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + } + } +} diff --git a/core/src/test/scala/laserdisc/protocol/KeyPSpec.scala b/core/src/test/scala/laserdisc/protocol/KeyPSpec.scala new file mode 100644 index 00000000..981bd2e4 --- /dev/null +++ b/core/src/test/scala/laserdisc/protocol/KeyPSpec.scala @@ -0,0 +1,457 @@ +package laserdisc +package protocol + +final class KeyPSpec extends KeyExtPSpec { + import keytypes._ + import org.scalacheck.{Arbitrary, Gen} + import org.scalacheck.Gen.const + + private[this] implicit final val encodingShow: Show[KeyEncoding] = Show.unsafeFromToString + private[this] implicit final val typeShow: Show[KeyType] = Show.unsafeFromToString + + private[this] implicit final val encodingArb: Arbitrary[KeyEncoding] = Arbitrary { + Gen.oneOf( + KeyEncoding.raw, + KeyEncoding.int, + KeyEncoding.ziplist, + KeyEncoding.intset, + KeyEncoding.hashtable, + KeyEncoding.skiplist + ) + } + private[this] implicit final val restoreEvictionArb: Arbitrary[KeyRestoreEviction] = Arbitrary { + nonNegIntArb.arbitrary.flatMap(nni => Gen.oneOf(KeyIdleTimeEviction(nni), KeyFrequencyEviction(nni))) + } + private[this] implicit final val restoreModeArb: Arbitrary[KeyRestoreMode] = Arbitrary { + Gen.oneOf(KeyRestoreMode.replace, KeyRestoreMode.absolutettl, KeyRestoreMode.both) + } + private[this] implicit final val ttlResponseArb: Arbitrary[KeyTTLResponse] = Arbitrary { + Gen.oneOf( + const(KeyNoKeyTTLResponse), + const(KeyNoExpireTTLResponse), + nonNegLongArb.arbitrary.map(KeyExpireAfterTTLResponse(_)) + ) + } + private[this] implicit final val typeArb: Arbitrary[KeyType] = Arbitrary { + Gen.oneOf(KeyType.string, KeyType.list, KeyType.set, KeyType.zset, KeyType.hash) + } + + private[this] final val ttlResponseToNum: KeyTTLResponse => Num = { + case KeyNoKeyTTLResponse => Num(-2L) + case KeyNoExpireTTLResponse => Num(-1L) + case KeyExpireAfterTTLResponse(ttl) => Num(ttl.value) + } + private[this] final val scanKeyToArr: Scan[Key] => Arr = scanKey => + Arr( + Bulk(scanKey.cursor.value), + scanKey.values.fold(NilArr: GenArr)(ks => Arr(ks.map(k => Bulk(k.value)).toList)) + ) + + "The Key protocol" when { + + "using del" should { + + "roundtrip successfully" when { + "given keys" in forAll("keys", "deleted") { (ks: OneOrMoreKeys, nni: NonNegInt) => + val protocol = del(ks) + + protocol.encode shouldBe Arr(Bulk("DEL") :: ks.value.map(Bulk(_))) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + } + } + } + + "using dump" should { + + "roundtrip successfully" when { + "given a key" in forAll("key", "dump") { (k: Key, os: Option[String]) => + val protocol = dump(k) + + protocol.encode shouldBe Arr(Bulk("DUMP"), Bulk(k)) + protocol.decode(os.fold(NullBulk: GenBulk)(Bulk(_))).right.value shouldBe os.map(Bulk(_)) + } + } + } + + "using exists" should { + + "roundtrip successfully" when { + "given keys" in forAll("keys", "exists") { (ks: OneOrMoreKeys, opi: Option[PosInt]) => + val protocol = exists(ks) + + protocol.encode shouldBe Arr(Bulk("EXISTS") :: ks.value.map(Bulk(_))) + protocol.decode(Num(opi.fold(0L)(_.value.toLong))).right.value shouldBe opi + } + } + } + + "using expire" should { + + "roundtrip successfully" when { + "given key and expiration" in forAll("key", "expiration", "expired") { (k: Key, nni: NonNegInt, b: Boolean) => + val protocol = expire(k, nni) + + protocol.encode shouldBe Arr(Bulk("EXPIRE"), Bulk(k), Bulk(nni)) + protocol.decode(boolToNum(b)).right.value shouldBe b + } + } + } + + "using expireat" should { + + "roundtrip successfully" when { + "given key and expire timestamp" in forAll("key", "timestamp", "expired") { (k: Key, nni: NonNegInt, b: Boolean) => + val protocol = expireat(k, nni) + + protocol.encode shouldBe Arr(Bulk("EXPIREAT"), Bulk(k), Bulk(nni)) + protocol.decode(boolToNum(b)).right.value shouldBe b + } + } + } + + "using keys" should { + + "roundtrip successfully" when { + "given glob pattern" in forAll("glob pattern", "keys") { (g: GlobPattern, ks: List[Key]) => + val protocol = keys(g) + + protocol.encode shouldBe Arr(Bulk("KEYS"), Bulk(g)) + protocol.decode(Arr(ks.map(Bulk(_)))).right.value shouldBe ks + } + } + } + + "using migrate" should { + + "roundtrip successfully" when { + "given key, host, port, db index and timeout" in forAll("key", "host", "port", "db index", "timeout", "response") { + (k: Key, h: Host, p: Port, dbi: DbIndex, nni: NonNegInt, nkOrOk: NOKEY | OK) => + val protocol = migrate(k, h, p, dbi, nni) + + protocol.encode shouldBe Arr(Bulk("MIGRATE"), Bulk(h), Bulk(p), Bulk(k), Bulk(dbi), Bulk(nni)) + protocol.decode(noKeyOrOkToStr(nkOrOk)).right.value shouldBe nkOrOk + } + "given keys, host, port, db index and timeout" in forAll("keys", "host", "port", "db index", "timeout", "response") { + (ks: TwoOrMoreKeys, h: Host, p: Port, dbi: DbIndex, nni: NonNegInt, nkOrOk: NOKEY | OK) => + val protocol = migrate(ks, h, p, dbi, nni) + + protocol.encode shouldBe Arr( + Bulk("MIGRATE") :: Bulk(h) :: Bulk(p) :: Bulk("") :: Bulk(dbi) :: Bulk(nni) :: Bulk("KEYS") :: ks.value.map(Bulk(_)) + ) + protocol.decode(noKeyOrOkToStr(nkOrOk)).right.value shouldBe nkOrOk + } + "given key, host, port, db index, timeout and migrate mode" in { + forAll("key", "host", "port", "db index", "timeout", "response") { + (k: Key, h: Host, p: Port, dbi: DbIndex, nni: NonNegInt, nkOrOk: NOKEY | OK) => + forAll("migrate mode") { mm: KeyMigrateMode => + val protocol = migrate(k, h, p, dbi, nni, mm) + + protocol.encode shouldBe Arr( + Bulk("MIGRATE") :: Bulk(h) :: Bulk(p) :: Bulk(k) :: Bulk(dbi) :: Bulk(nni) :: mm.params.map(Bulk(_)) + ) + protocol.decode(noKeyOrOkToStr(nkOrOk)).right.value shouldBe nkOrOk + } + } + } + "given keys, host, port, db index, timeout and migrate mode" in { + forAll("keys", "host", "port", "db index", "timeout", "response") { + (ks: TwoOrMoreKeys, h: Host, p: Port, dbi: DbIndex, nni: NonNegInt, nkOrOk: NOKEY | OK) => + forAll("migrate mode") { mm: KeyMigrateMode => + val protocol = migrate(ks, h, p, dbi, nni, mm) + + protocol.encode shouldBe Arr( + Bulk("MIGRATE") :: Bulk(h) :: Bulk(p) :: Bulk("") :: Bulk(dbi) :: Bulk(nni) :: + mm.params.map(Bulk(_)) ::: (Bulk("KEYS") :: ks.value.map(Bulk(_))) + ) + protocol.decode(noKeyOrOkToStr(nkOrOk)).right.value shouldBe nkOrOk + } + } + } + } + } + + "using move" should { + + "roundtrip successfully" when { + "given key and db" in forAll("key", "db", "moved") { (k: Key, db: DbIndex, b: Boolean) => + val protocol = move(k, db) + + protocol.encode shouldBe Arr(Bulk("MOVE"), Bulk(k), Bulk(db)) + protocol.decode(boolToNum(b)).right.value shouldBe b + } + } + } + + "using obj.encoding" should { + + "roundtrip successfully" when { + "given key" in forAll("key", "encoding") { (k: Key, oe: Option[KeyEncoding]) => + val protocol = obj.encoding(k) + + protocol.encode shouldBe Arr(Bulk("OBJECT"), Bulk("ENCODING"), Bulk(k)) + protocol.decode(oe.fold(NullBulk: GenBulk)(Bulk(_))).right.value shouldBe oe + } + } + } + + "using obj.freq" should { + + "roundtrip successfully" when { + "given key" in forAll("key", "frequency") { (k: Key, nni: NonNegInt) => + val protocol = obj.freq(k) + + protocol.encode shouldBe Arr(Bulk("OBJECT"), Bulk("FREQ"), Bulk(k)) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + } + } + } + + "using obj.idletime" should { + + "roundtrip successfully" when { + "given key" in forAll("key", "idle time") { (k: Key, nni: NonNegInt) => + val protocol = obj.idletime(k) + + protocol.encode shouldBe Arr(Bulk("OBJECT"), Bulk("IDLETIME"), Bulk(k)) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + } + } + } + + "using obj.refcount" should { + + "roundtrip successfully" when { + "given key" in forAll("key", "ref count") { (k: Key, nni: NonNegInt) => + val protocol = obj.refcount(k) + + protocol.encode shouldBe Arr(Bulk("OBJECT"), Bulk("REFCOUNT"), Bulk(k)) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + } + } + } + + "using persist" should { + + "roundtrip successfully" when { + "given key" in forAll("key", "persisted") { (k: Key, b: Boolean) => + val protocol = persist(k) + + protocol.encode shouldBe Arr(Bulk("PERSIST"), Bulk(k)) + protocol.decode(boolToNum(b)).right.value shouldBe b + } + } + } + + "using pexpire" should { + + "roundtrip successfully" when { + "given key" in forAll("key", "timeout", "persisted") { (k: Key, nnl: NonNegLong, b: Boolean) => + val protocol = pexpire(k, nnl) + + protocol.encode shouldBe Arr(Bulk("PEXPIRE"), Bulk(k), Bulk(nnl)) + protocol.decode(boolToNum(b)).right.value shouldBe b + } + } + } + + "using pexpireat" should { + + "roundtrip successfully" when { + "given key" in forAll("key", "timestamp", "persisted") { (k: Key, nnl: NonNegLong, b: Boolean) => + val protocol = pexpireat(k, nnl) + + protocol.encode shouldBe Arr(Bulk("PEXPIREAT"), Bulk(k), Bulk(nnl)) + protocol.decode(boolToNum(b)).right.value shouldBe b + } + } + } + + "using pttl" should { + + "roundtrip successfully" when { + "given key" in forAll("key", "ttl") { (k: Key, ttl: KeyTTLResponse) => + val protocol = pttl(k) + + protocol.encode shouldBe Arr(Bulk("PTTL"), Bulk(k)) + protocol.decode(ttlResponseToNum(ttl)).right.value shouldBe ttl + } + } + } + + "using randomkey" should { + + "roundtrip successfully" when { + "using val" in { (ok: Option[Key]) => + val protocol = randomkey + + protocol.encode shouldBe Arr(Bulk("RANDOMKEY")) + protocol.decode(ok.fold(NullBulk: GenBulk)(Bulk(_))).right.value shouldBe ok + } + } + } + + "using rename" should { + + "roundtrip successfully" when { + "given old key and new key" in forAll("old key", "new key") { (ok: Key, nk: Key) => + val protocol = rename(ok, nk) + + protocol.encode shouldBe Arr(Bulk("RENAME"), Bulk(ok), Bulk(nk)) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + } + + "using renamenx" should { + + "roundtrip successfully" when { + "given old key and new key" in forAll("old key", "new key", "renamed") { (ok: Key, nk: Key, b: Boolean) => + val protocol = renamenx(ok, nk) + + protocol.encode shouldBe Arr(Bulk("RENAMENX"), Bulk(ok), Bulk(nk)) + protocol.decode(boolToNum(b)).right.value shouldBe b + } + } + } + + "using restore" should { + + "roundtrip successfully" when { + "given key, ttl and serialized value" in forAll("key", "ttl", "serialized value") { (k: Key, nnl: NonNegLong, s: String) => + val protocol = restore(k, nnl, Bulk(s)) + + protocol.encode shouldBe Arr(Bulk("RESTORE"), Bulk(k), Bulk(nnl), Bulk(s)) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + "given key, ttl, serialized value and mode" in { + forAll("key", "ttl", "serialized value", "mode") { (k: Key, nnl: NonNegLong, s: String, m: KeyRestoreMode) => + val protocol = restore(k, nnl, Bulk(s), m) + + protocol.encode shouldBe Arr(Bulk("RESTORE") :: Bulk(k) :: Bulk(nnl) :: Bulk(s) :: m.params.map(Bulk(_))) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + "given key, ttl, serialized value and eviction" in { + forAll("key", "ttl", "serialized value", "eviction") { (k: Key, nnl: NonNegLong, s: String, e: KeyRestoreEviction) => + val protocol = restore(k, nnl, Bulk(s), e) + + protocol.encode shouldBe Arr(Bulk("RESTORE"), Bulk(k), Bulk(nnl), Bulk(s), Bulk(e.param), Bulk(e.seconds)) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + "given key, ttl, serialized value, mode and eviction" in forAll("key", "ttl", "serialized value", "mode", "eviction") { + (k: Key, nnl: NonNegLong, s: String, m: KeyRestoreMode, e: KeyRestoreEviction) => + val protocol = restore(k, nnl, Bulk(s), m, e) + + protocol.encode shouldBe Arr( + Bulk("RESTORE") :: Bulk(k) :: Bulk(nnl) :: Bulk(s) :: m.params.map(Bulk(_)) ::: (Bulk(e.param) :: Bulk(e.seconds) :: Nil) + ) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + } + + "using scan" should { + + "roundtrip successfully" when { + "given cursor" in forAll("cursor", "scan result") { (nnl: NonNegLong, sk: Scan[Key]) => + val protocol = scan(nnl) + + protocol.encode shouldBe Arr(Bulk("SCAN"), Bulk(nnl)) + protocol.decode(scanKeyToArr(sk)).right.value shouldBe sk + } + "given cursor and glob pattern" in { + forAll("cursor", "glob pattern", "scan result") { (nnl: NonNegLong, g: GlobPattern, sk: Scan[Key]) => + val protocol = scan(nnl, g) + + protocol.encode shouldBe Arr(Bulk("SCAN"), Bulk(nnl), Bulk("MATCH"), Bulk(g)) + protocol.decode(scanKeyToArr(sk)).right.value shouldBe sk + } + } + "given cursor and count" in { + forAll("cursor", "count", "scan result") { (nnl: NonNegLong, pi: PosInt, sk: Scan[Key]) => + val protocol = scan(nnl, pi) + + protocol.encode shouldBe Arr(Bulk("SCAN"), Bulk(nnl), Bulk("COUNT"), Bulk(pi)) + protocol.decode(scanKeyToArr(sk)).right.value shouldBe sk + } + } + "given cursor, glob pattern and count" in forAll("cursor", "glob pattern", "count", "scan result") { + (nnl: NonNegLong, g: GlobPattern, pi: PosInt, sk: Scan[Key]) => + val protocol = scan(nnl, g, pi) + + protocol.encode shouldBe Arr(Bulk("SCAN"), Bulk(nnl), Bulk("MATCH"), Bulk(g), Bulk("COUNT"), Bulk(pi)) + protocol.decode(scanKeyToArr(sk)).right.value shouldBe sk + } + } + } + + //FIXME add SORT + + "using touch" should { + + "roundtrip successfully" when { + "given keys" in forAll("keys", "touched") { (ks: OneOrMoreKeys, nni: NonNegInt) => + val protocol = touch(ks) + + protocol.encode shouldBe Arr(Bulk("TOUCH") :: ks.value.map(Bulk(_))) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + } + } + } + + "using ttl" should { + + "roundtrip successfully" when { + "given key" in forAll("key", "ttl") { (k: Key, ttl0: KeyTTLResponse) => + val protocol = ttl(k) + + protocol.encode shouldBe Arr(Bulk("TTL"), Bulk(k)) + protocol.decode(ttlResponseToNum(ttl0)).right.value shouldBe ttl0 + } + } + } + + "using typeof" should { + + "roundtrip successfully" when { + "given key" in forAll("key", "type") { (k: Key, ot: Option[KeyType]) => + val protocol = typeof(k) + + protocol.encode shouldBe Arr(Bulk("TYPE"), Bulk(k)) + protocol.decode(ot.fold(Str("none"))(Str(_))).right.value shouldBe ot + } + } + } + + "using unlink" should { + + "roundtrip successfully" when { + "given keys" in forAll("keys", "unlinked") { (ks: OneOrMoreKeys, nni: NonNegInt) => + val protocol = unlink(ks) + + protocol.encode shouldBe Arr(Bulk("UNLINK") :: ks.value.map(Bulk(_))) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + } + } + } + + "using wait" should { + + "roundtrip successfully" when { + "given replicas" in forAll("replicas", "acknowledgements") { (pi1: PosInt, pi2: PosInt) => + val protocol = wait(pi1) + + protocol.encode shouldBe Arr(Bulk("WAIT"), Bulk(pi1), Bulk(0)) + protocol.decode(Num(pi2.value.toLong)).right.value shouldBe pi2 + } + "given replicas and timeout" in forAll("replicas", "timeout", "acknowledgements") { (pi1: PosInt, pl: PosLong, pi2: PosInt) => + val protocol = wait(pi1, pl) + + protocol.encode shouldBe Arr(Bulk("WAIT"), Bulk(pi1), Bulk(pl)) + protocol.decode(Num(pi2.value.toLong)).right.value shouldBe pi2 + } + } + } + } +} diff --git a/core/src/test/scala/laserdisc/protocol/ListPSpec.scala b/core/src/test/scala/laserdisc/protocol/ListPSpec.scala new file mode 100644 index 00000000..8fc2f50b --- /dev/null +++ b/core/src/test/scala/laserdisc/protocol/ListPSpec.scala @@ -0,0 +1,188 @@ +package laserdisc +package protocol + +final class ListPSpec extends ListExtPSpec { + import listtypes._ + import org.scalacheck.{Arbitrary, Gen} + + private[this] implicit final val positionArb: Arbitrary[ListPosition] = Arbitrary { + Gen.oneOf(ListPosition.after, ListPosition.before) + } + + "The List protocol" when { + + "using lindex" should { + + "roundtrip successfully" when { + "given key and index" in forAll("key", "index", "value") { (k: Key, i: Index, oi: Option[Int]) => + val protocol = lindex[Int](k, i) + + protocol.encode shouldBe Arr(Bulk("LINDEX"), Bulk(k), Bulk(i)) + protocol.decode(oi.fold(NullBulk: GenBulk)(Bulk(_))).right.value shouldBe oi + } + } + } + + "using linsert" should { + + "roundtrip successfully" when { + "given key, position, pivot and value" in { + forAll("key", "position", "pivot", "value", "inserted") { (k: Key, p: ListPosition, pi: String, v: String, opi: Option[PosInt]) => + val protocol = linsert(k, p, pi, v) + + protocol.encode shouldBe Arr(Bulk("LINSERT"), Bulk(k), Bulk(p), Bulk(pi), Bulk(v)) + protocol.decode(Num(opi.fold(-1L)(_.value.toLong))).right.value shouldBe opi + } + } + } + } + + "using llen" should { + + "roundtrip successfully" when { + "given key" in forAll("key", "length") { (k: Key, nni: NonNegInt) => + val protocol = llen(k) + + protocol.encode shouldBe Arr(Bulk("LLEN"), Bulk(k)) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + } + } + } + + "using lpop" should { + + "roundtrip successfully" when { + "given key" in forAll("key", "popped value") { (k: Key, os: Option[String]) => + val protocol = lpop[String](k) + + protocol.encode shouldBe Arr(Bulk("LPOP"), Bulk(k)) + protocol.decode(os.fold(NullBulk: GenBulk)(Bulk(_))).right.value shouldBe os + } + } + } + + "using lpush" should { + + "roundtrip successfully" when { + "given key and values" in forAll("key", "values", "pushed") { (k: Key, is: OneOrMore[Int], pi: PosInt) => + val protocol = lpush(k, is) + + protocol.encode shouldBe Arr(Bulk("LPUSH") :: Bulk(k) :: is.value.map(Bulk(_))) + protocol.decode(Num(pi.value.toLong)).right.value shouldBe pi + } + } + } + + "using lpushx" should { + + "roundtrip successfully" when { + "given key and value" in forAll("key", "value", "pushed") { (k: Key, i: Int, opi: Option[PosInt]) => + val protocol = lpushx(k, i) + + protocol.encode shouldBe Arr(Bulk("LPUSHX"), Bulk(k), Bulk(i)) + protocol.decode(Num(opi.fold(0L)(_.value.toLong))).right.value shouldBe opi + } + } + } + + "using lrange" should { + + "roundtrip successfully" when { + "given key, start index and end index" in { + forAll("key", "start index", "end index", "values") { (k: Key, si: Index, ei: Index, is: List[Int]) => + val protocol = lrange[Int](k, si, ei) + + protocol.encode shouldBe Arr(Bulk("LRANGE"), Bulk(k), Bulk(si), Bulk(ei)) + protocol.decode(Arr(is.map(Bulk(_)))).right.value shouldBe is + } + } + } + } + + "using lrem" should { + + "roundtrip successfully" when { + "given key, count and value" in forAll("key", "count", "value", "removed") { (k: Key, i: Index, s: String, nni: NonNegInt) => + val protocol = lrem(k, i, s) + + protocol.encode shouldBe Arr(Bulk("LREM"), Bulk(k), Bulk(i), Bulk(s)) + protocol.decode(Num(nni.value.toLong)).right.value shouldBe nni + } + } + } + + "using lset" should { + + "roundtrip successfully" when { + "given key, count and value" in forAll("key", "count", "value") { (k: Key, i: Index, s: String) => + val protocol = lset(k, i, s) + + protocol.encode shouldBe Arr(Bulk("LSET"), Bulk(k), Bulk(i), Bulk(s)) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + } + + "using ltrim" should { + + "roundtrip successfully" when { + "given key, start index and end index" in forAll("key", "start index", "end index") { (k: Key, si: Index, ei: Index) => + val protocol = ltrim(k, si, ei) + + protocol.encode shouldBe Arr(Bulk("LTRIM"), Bulk(k), Bulk(si), Bulk(ei)) + protocol.decode(Str(OK.value)).right.value shouldBe OK + } + } + } + + "using rpop" should { + + "roundtrip successfully" when { + "given key" in forAll("key", "popped value") { (k: Key, os: Option[String]) => + val protocol = rpop[String](k) + + protocol.encode shouldBe Arr(Bulk("RPOP"), Bulk(k)) + protocol.decode(os.fold(NullBulk: GenBulk)(Bulk(_))).right.value shouldBe os + } + } + } + + "using rpoplpush" should { + + "roundtrip successfully" when { + "given source key and destination key" in { + forAll("source key", "destination", "popped value") { (sk: Key, dk: Key, os: Option[String]) => + val protocol = rpoplpush[String](sk, dk) + + protocol.encode shouldBe Arr(Bulk("RPOPLPUSH"), Bulk(sk), Bulk(dk)) + protocol.decode(os.fold(NullBulk: GenBulk)(Bulk(_))).right.value shouldBe os + } + } + } + } + + "using rpush" should { + + "roundtrip successfully" when { + "given key and values" in forAll("key", "values", "pushed") { (k: Key, is: OneOrMore[Int], pi: PosInt) => + val protocol = rpush(k, is) + + protocol.encode shouldBe Arr(Bulk("RPUSH") :: Bulk(k) :: is.value.map(Bulk(_))) + protocol.decode(Num(pi.value.toLong)).right.value shouldBe pi + } + } + } + + "using rpushx" should { + + "roundtrip successfully" when { + "given key and value" in forAll("key", "value", "pushed") { (k: Key, i: Int, opi: Option[PosInt]) => + val protocol = rpushx(k, i) + + protocol.encode shouldBe Arr(Bulk("RPUSHX"), Bulk(k), Bulk(i)) + protocol.decode(Num(opi.fold(0L)(_.value.toLong))).right.value shouldBe opi + } + } + } + } +} diff --git a/core/src/test/scala/laserdisc/protocol/RESPCodecsSpec.scala b/core/src/test/scala/laserdisc/protocol/RESPCodecsSpec.scala index 52bde5fc..cffc7883 100644 --- a/core/src/test/scala/laserdisc/protocol/RESPCodecsSpec.scala +++ b/core/src/test/scala/laserdisc/protocol/RESPCodecsSpec.scala @@ -3,21 +3,15 @@ package protocol import java.nio.charset.StandardCharsets.UTF_8 -import org.scalacheck.Arbitrary.arbitrary -import org.scalacheck.Gen.{chooseNum, listOfN, option, oneOf => genOneOf} import org.scalacheck.{Arbitrary, Gen} -import org.scalatest.{EitherValues, MustMatchers, WordSpec} -import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks +import org.scalacheck.Arbitrary.arbitrary +import org.scalacheck.Gen._ +import org.scalatest.EitherValues import scodec.bits.BitVector -import scodec.{Codec, Err} +import scodec.{Codec, Err => SErr} object RESPCodecsSpec extends EitherValues { - private final case class InvalidDiscriminator(c: Char) { - val toHex: String = BitVector.fromByte(c.toByte).toHex - override val toString: String = s"$c" - } - private[this] final object functions { private[this] final val attemptDecode = (bits: BitVector) => Codec[RESP].decodeValue(bits) private[this] final val requireEncode = (resp: RESP) => Codec[RESP].encode(resp).require @@ -40,136 +34,147 @@ object RESPCodecsSpec extends EitherValues { final val roundTripAttempt = requireEncode andThen attemptDecode } - implicit final class RichString(private val underlying: String) extends AnyVal { - def RESP: Err | RESP = functions.stringToRESPAttempt(underlying).toEither - def asRESP: RESP = RESP.right.value - def bytesLength: Int = functions.stringToBytesLength(underlying) + private implicit final class RichChar(private val underlying: Char) extends AnyVal { + def toHex: String = BitVector.fromByte(underlying.toByte).toHex } - implicit final class RichRESP(private val underlying: RESP) extends AnyVal { + private implicit final class RichString(private val underlying: String) extends AnyVal { + def RESP: SErr | RESP = functions.stringToRESPAttempt(underlying).toEither + def asRESP: RESP = RESP.right.value + def bytesLength: Int = functions.stringToBytesLength(underlying) + } + + private implicit final class RichRESP(private val underlying: RESP) extends AnyVal { def wireFormat: String = functions.respToString(underlying) def roundTrip: RESP = functions.roundTripAttempt(underlying).require } - implicit final class RichSeqRESP(private val underlying: Seq[RESP]) extends AnyVal { + private implicit final class RichSeqRESP(private val underlying: Seq[RESP]) extends AnyVal { def wireFormat: String = functions.respSeqToString(underlying) } } -final class RESPCodecsSpec extends WordSpec with MustMatchers with ScalaCheckPropertyChecks with EitherValues { - import RESP._ +final class RESPCodecsSpec extends BaseSpec { import RESPCodecsSpec._ - private[this] val smallListSize = chooseNum(0, 20) - private[this] implicit def invalidProtocolDiscriminator: Gen[InvalidDiscriminator] = { + private[this] val smallNumGen: Gen[Int] = chooseNum(0, 20) + private[this] val invalidProtocolGen: Gen[Char] = { val exclusions = List('+', '-', ':', '$', '*') - Gen.choose[Char](0, 127).suchThat(!exclusions.contains(_)).map(InvalidDiscriminator) - } - private[this] implicit val _ = Gen.choose(Char.MinValue, Char.MaxValue).filter(Character.isDefined) - private[this] implicit val genSimpleString: Gen[SimpleString] = arbitrary[String].map(str) - private[this] implicit val genError: Gen[Error] = arbitrary[String].map(err) - private[this] implicit val genInteger: Gen[Integer] = arbitrary[Long].map(int) - private[this] implicit val genBulkString: Gen[BulkString] = arbitrary[Option[String]].map { - case None => NullBulkString - case Some(s) => bulk(s) - } - private[this] implicit def genArray: Gen[Array] = smallListSize.flatMap { size => - option(listOfN(size, genRESP)).map { - case None => NilArray - case Some(v) => arr(v) - } - } - private[this] implicit def genRESP: Gen[RESP] = - genOneOf(genSimpleString, genError, genInteger, genBulkString, genArray) - private[this] implicit def genOptionListRESP: Gen[Option[List[RESP]]] = smallListSize.flatMap { size => - option(listOfN(size, genRESP)) - } - - private[this] implicit def arb[A](implicit A: Gen[A]): Arbitrary[A] = Arbitrary(A) + choose[Char](0, 127).suchThat(!exclusions.contains(_)) + } :| "invalid protocol discriminator" + private[this] val stringGen: Gen[String] = listOf(utf8BMPCharGen).map(_.mkString) :| "string" + private[this] val strGen: Gen[Str] = stringGen.map(Str.apply) :| "simple string RESP" + private[this] val errGen: Gen[Err] = stringGen.map(Err.apply) :| "error RESP" + private[this] val numGen: Gen[Num] = arbitrary[Long].map(Num.apply) :| "integer RESP" + private[this] val genBulkGen: Gen[GenBulk] = option(stringGen).map { + case None => NullBulk + case Some(s) => Bulk(s) + } :| "bulk string RESP" + private[this] def genArrGen: Gen[GenArr] = + smallNumGen.flatMap { size => + option(listOfN(size, respGen)).map { + case None => NilArr + case Some(v) => Arr(v) + } + } :| "array RESP" + private[this] def respGen: Gen[RESP] = + lzy { + frequency(2 -> strGen, 1 -> errGen, 2 -> numGen, 4 -> genBulkGen, 1 -> genArrGen) + } :| "RESP" + private[this] def respListGen: Gen[List[RESP]] = smallNumGen.flatMap(listOfN(_, respGen)) :| "list of RESPs" + + private[this] implicit final val stringArb: Arbitrary[String] = Arbitrary(stringGen) + private[this] implicit final val invalidProtocolArb: Arbitrary[Char] = Arbitrary(invalidProtocolGen) + private[this] implicit final val strArb: Arbitrary[Str] = Arbitrary(strGen) + private[this] implicit final val errArb: Arbitrary[Err] = Arbitrary(errGen) + private[this] implicit final val numArb: Arbitrary[Num] = Arbitrary(numGen) + private[this] implicit final val genBulkArb: Arbitrary[GenBulk] = Arbitrary(genBulkGen) + private[this] implicit final val genArrArb: Arbitrary[GenArr] = Arbitrary(genArrGen) + private[this] implicit final val respListArb: Arbitrary[List[RESP]] = Arbitrary(respListGen) "A RESP codec" when { - "handling unknown protocol type" must { - "fail with correct error message" in forAll { invalidDiscriminator: InvalidDiscriminator => - s"$invalidDiscriminator".RESP.left.value.messageWithContext mustBe s"unidentified RESP type (Hex: ${invalidDiscriminator.toHex})" + "handling unknown protocol type" should { + "fail with correct error message" in forAll { c: Char => + s"$c".RESP.left.value.messageWithContext shouldBe s"unidentified RESP type (Hex: ${c.toHex})" } } - "handling simple strings" must { + "handling simple strings" should { "decode them correctly" in forAll { s: String => - s"+$s\r\n".asRESP mustBe str(s) + s"+$s$CRLF".asRESP shouldBe Str(s) } - "encode them correctly" in forAll { simpleString: SimpleString => - simpleString.wireFormat mustBe s"+${simpleString.value}\r\n" + "encode them correctly" in forAll { s: Str => + s.wireFormat shouldBe s"+${s.value}$CRLF" } - "round-trip with no errors" in forAll { simpleString: SimpleString => - simpleString.roundTrip mustBe simpleString + "roundtrip with no errors" in forAll { s: Str => + s.roundTrip shouldBe s } } - "handling errors" must { - "decode them correctly" in forAll { msg: String => - s"-$msg\r\n".asRESP mustBe err(msg) + "handling errors" should { + "decode them correctly" in forAll { s: String => + s"-$s$CRLF".asRESP shouldBe Err(s) } - "encode them correctly" in forAll { error: Error => - error.wireFormat mustBe s"-${error.message}\r\n" + "encode them correctly" in forAll { e: Err => + e.wireFormat shouldBe s"-${e.message}$CRLF" } - "round-trip with no errors" in forAll { error: Error => - error.roundTrip mustBe error + "roundtrip with no errors" in forAll { e: Err => + e.roundTrip shouldBe e } } - "handling integers" must { + "handling integers" should { "decode them correctly" in forAll { l: Long => - s":$l\r\n".asRESP mustBe int(l) + s":$l$CRLF".asRESP shouldBe Num(l) } - "encode them correctly" in forAll { integer: Integer => - integer.wireFormat mustBe s":${integer.value}\r\n" + "encode them correctly" in forAll { n: Num => + n.wireFormat shouldBe s":${n.value}$CRLF" } - "round-trip with no errors" in forAll { integer: Integer => - integer.roundTrip mustBe integer + "roundtrip with no errors" in forAll { n: Num => + n.roundTrip shouldBe n } } - "handling bulk strings" must { + "handling bulk strings" should { "fail with correct error message when decoding size < -1" in { - "$-2\r\nbla\r\n".RESP.left.value.messageWithContext mustBe "size: failed to decode bulk-string of size -2" + s"$$-2${CRLF}bla$CRLF".RESP.left.value.messageWithContext shouldBe "size: failed to decode bulk-string of size -2" } - "decode them correctly" in forAll { maybeString: Option[String] => - maybeString match { - case None => "$-1\r\n".asRESP mustBe nullBulk - case Some(s) => s"$$${s.bytesLength}\r\n$s\r\n".asRESP mustBe bulk(s) + "decode them correctly" in forAll { os: Option[String] => + os match { + case None => s"$$-1$CRLF".asRESP shouldBe NullBulk + case Some(s) => s"$$${s.bytesLength}$CRLF$s$CRLF".asRESP shouldBe Bulk(s) } } - "encode them correctly" in forAll { bulkString: BulkString => - bulkString -> bulkString.wireFormat match { - case (NullBulkString, s) => s mustBe "$-1\r\n" - case (NonNullBulkString(bs), s) => s mustBe s"$$${bs.bytesLength}\r\n$bs\r\n" + "encode them correctly" in forAll { b: GenBulk => + b match { + case NullBulk => b.wireFormat shouldBe s"$$-1$CRLF" + case Bulk(bs) => b.wireFormat shouldBe s"$$${bs.bytesLength}$CRLF$bs$CRLF" } } - "round-trip with no errors" in forAll { bulkString: BulkString => - bulkString.roundTrip mustBe bulkString + "roundtrip with no errors" in forAll { b: GenBulk => + b.roundTrip shouldBe b } } - "handling arrays" must { + "handling arrays" should { "fail with correct error message when decoding size < -1" in { - "*-2\r\nbla\r\n".RESP.left.value.messageWithContext mustBe "size: failed to decode array of size -2" + s"*-2${CRLF}bla$CRLF".RESP.left.value.messageWithContext shouldBe "size: failed to decode array of size -2" } - "decode them correctly" in forAll { maybeRESPList: Option[List[RESP]] => - maybeRESPList match { - case None => "*-1\r\n".asRESP mustBe nilArray - case Some(xs) => s"*${xs.length}\r\n${xs.wireFormat}".asRESP mustBe arr(xs) + "decode them correctly" in forAll { ors: Option[List[RESP]] => + ors match { + case None => s"*-1$CRLF".asRESP shouldBe NilArr + case Some(xs) => s"*${xs.length}$CRLF${xs.wireFormat}".asRESP shouldBe Arr(xs) } } - "encode them correctly" in forAll { array: Array => - array -> array.wireFormat match { - case (NilArray, s) => s mustBe "*-1\r\n" - case (NonNilArray(xs), s) => s mustBe s"*${xs.length}\r\n${xs.wireFormat}" + "encode them correctly" in forAll { a: GenArr => + a match { + case NilArr => a.wireFormat shouldBe s"*-1$CRLF" + case Arr(xs) => a.wireFormat shouldBe s"*${xs.length}$CRLF${xs.wireFormat}" } } - "round-trip with no errors" in forAll { array: Array => - array.roundTrip mustBe array + "roundtrip with no errors" in forAll { a: GenArr => + a.roundTrip shouldBe a } } } diff --git a/core/src/test/scala/laserdisc/protocol/RESPFrameArrSpec.scala b/core/src/test/scala/laserdisc/protocol/RESPFrameArrSpec.scala new file mode 100644 index 00000000..dfc88679 --- /dev/null +++ b/core/src/test/scala/laserdisc/protocol/RESPFrameArrSpec.scala @@ -0,0 +1,164 @@ +package laserdisc +package protocol + +import scodec.bits.BitVector + +final class RESPFrameArrSpec extends BaseSpec { + + "An empty GenArr Frame" when { + + "appending a bit vector that represent an empty array" should { + "produce a complete frame with the bits of an empty bulk" in { + val inputVector = BitVector("*0\r\n".getBytes) + EmptyFrame.append(inputVector.toByteBuffer) should be(Right(CompleteFrame(inputVector))) + } + } + } + + "A non empty GenArr Frame" when { + + "appending a bit vector that completes it" should { + "produce a complete frame with the correct bits" in { + val nonEmptyFrame = IncompleteFrame(BitVector("*1\r\n$16\r\nTest bulk str".getBytes), 0) + val inputVector = BitVector("ing\r\n".getBytes) + val expected = BitVector("*1\r\n$16\r\nTest bulk string\r\n".getBytes) + nonEmptyFrame.append(inputVector.toByteBuffer) should be(Right(CompleteFrame(expected))) + } + } + + "appending a bit vector that doesn't complete the array but has complete objects" should { + "produce an incomplete frame with the correct partial and 0 as missing count" in { + val nonEmptyFrame = IncompleteFrame(BitVector("*3\r\n$16\r\nTest bulk str".getBytes), 0) + val inputVector = BitVector("ing\r\n:100\r\n".getBytes) + val expected = BitVector("*3\r\n$16\r\nTest bulk string\r\n:100\r\n".getBytes) + nonEmptyFrame.append(inputVector.toByteBuffer) should be(Right(IncompleteFrame(expected, 0))) + } + } + + "appending a bit vector that completes the array" should { + "produce a complete frame with the correct bits" in { + val nonEmptyFrame = IncompleteFrame(BitVector("*3\r\n$16\r\nTest bulk str".getBytes), 0) + val inputVector = BitVector("ing\r\n:100\r\n+A simple string\r\n".getBytes) + val expected = BitVector("*3\r\n$16\r\nTest bulk string\r\n:100\r\n+A simple string\r\n".getBytes) + nonEmptyFrame.append(inputVector.toByteBuffer) should be(Right(CompleteFrame(expected))) + } + } + + "appending a bit vector with more than one array" should { + "produce more than one frame with a list of the complete ones and an empty remainder" in { + val nonEmptyFrame = IncompleteFrame(BitVector("*3\r\n$16\r\nTest bulk str".getBytes), 0) + val inputVector = BitVector("ing\r\n:100\r\n+A simple string\r\n*2\r\n$8\r\nAnother1\r\n-An error\r\n".getBytes) + nonEmptyFrame.append(inputVector.toByteBuffer) should be( + Right( + MoreThanOneFrame( + CompleteFrame(BitVector("*2\r\n$8\r\nAnother1\r\n-An error\r\n".getBytes)) :: + CompleteFrame(BitVector("*3\r\n$16\r\nTest bulk string\r\n:100\r\n+A simple string\r\n".getBytes)) :: Nil, + BitVector.empty + )) + ) + } + } + + "appending a bit vector with more than one array plus a reminder" should { + "produce more than one frame with a list of the complete ones and the correct remainder" in { + val nonEmptyFrame = IncompleteFrame(BitVector("*3\r\n$16\r\nTest bulk str".getBytes), 0) + val inputVector = BitVector("ing\r\n:100\r\n+A simple string\r\n*2\r\n$8\r\nAnother1\r\n-An error\r\n$17\r\nAnother bulk ".getBytes) + nonEmptyFrame.append(inputVector.toByteBuffer) should be( + Right( + MoreThanOneFrame( + CompleteFrame(BitVector("*2\r\n$8\r\nAnother1\r\n-An error\r\n".getBytes)) :: + CompleteFrame(BitVector("*3\r\n$16\r\nTest bulk string\r\n:100\r\n+A simple string\r\n".getBytes)) :: Nil, + BitVector("$17\r\nAnother bulk ".getBytes()) + )) + ) + } + } + + "appending a bit vector with multiple null arrays" should { + "produce more than one frame with a list of the complete ones and the correct remainder" in { + val nonEmptyFrame = IncompleteFrame(BitVector("*3\r\n$16\r\nTest bulk str".getBytes), 0) + val inputVector = BitVector("ing\r\n:100\r\n+A simple string\r\n*-1\r\n*-1\r\n*-1\r\n*-1\r\n*-1\r\n".getBytes) + nonEmptyFrame.append(inputVector.toByteBuffer) should be( + Right( + MoreThanOneFrame( + CompleteFrame(BitVector("*-1\r\n".getBytes)) :: + CompleteFrame(BitVector("*-1\r\n".getBytes)) :: + CompleteFrame(BitVector("*-1\r\n".getBytes)) :: + CompleteFrame(BitVector("*-1\r\n".getBytes)) :: + CompleteFrame(BitVector("*-1\r\n".getBytes)) :: + CompleteFrame(BitVector("*3\r\n$16\r\nTest bulk string\r\n:100\r\n+A simple string\r\n".getBytes)) :: Nil, + BitVector.empty + )) + ) + } + } + + "appending a bit vector with multiple null arrays the last of which not complete" should { + "produce more than one frame with a list of the complete ones and the correct remainder" in { + val nonEmptyFrame = IncompleteFrame(BitVector("*3\r\n$16\r\nTest bulk str".getBytes), 0) + val inputVector = BitVector("ing\r\n:100\r\n+A simple string\r\n*-1\r\n*-1\r\n*-1\r\n*-1\r\n*-1\r\n*".getBytes) + nonEmptyFrame.append(inputVector.toByteBuffer) should be( + Right( + MoreThanOneFrame( + CompleteFrame(BitVector("*-1\r\n".getBytes)) :: + CompleteFrame(BitVector("*-1\r\n".getBytes)) :: + CompleteFrame(BitVector("*-1\r\n".getBytes)) :: + CompleteFrame(BitVector("*-1\r\n".getBytes)) :: + CompleteFrame(BitVector("*-1\r\n".getBytes)) :: + CompleteFrame(BitVector("*3\r\n$16\r\nTest bulk string\r\n:100\r\n+A simple string\r\n".getBytes)) :: Nil, + BitVector("*".getBytes()) + )) + ) + } + } + + "appending a bit vector with multiple arrays interleaved with null arrays" should { + "produce as result of `complete` more than one frame with a list of the complete arrays in the correct order" in { + val nonEmptyFrame = IncompleteFrame(BitVector("*3\r\n$16\r\nTest bulk str".getBytes), 0) + val inputVector = + BitVector("ing\r\n:100\r\n+A simple string\r\n*-1\r\n*-1\r\n*2\r\n$8\r\nAnother1\r\n-An error\r\n*-1\r\n*-1\r\n*-1\r\n".getBytes) + nonEmptyFrame + .append(inputVector.toByteBuffer) + .fold( + err => fail(s"expected a result but failed with $err"), { + case r @ MoreThanOneFrame(_, _) => + r.complete shouldBe Vector( + CompleteFrame(BitVector("*3\r\n$16\r\nTest bulk string\r\n:100\r\n+A simple string\r\n".getBytes)), + CompleteFrame(BitVector("*-1\r\n".getBytes)), + CompleteFrame(BitVector("*-1\r\n".getBytes)), + CompleteFrame(BitVector("*2\r\n$8\r\nAnother1\r\n-An error\r\n".getBytes)), + CompleteFrame(BitVector("*-1\r\n".getBytes)), + CompleteFrame(BitVector("*-1\r\n".getBytes)), + CompleteFrame(BitVector("*-1\r\n".getBytes)) + ) + case _ => fail(s"expected a MoreThanOne type") + } + ) + } + } + + "appending a bit vector with multiple arrays containing nested arrays" should { + "produce as result of `complete` more than one frame with a list of the complete arrays in the correct order" in { + val nonEmptyFrame = IncompleteFrame(BitVector("*3\r\n$16\r\nTest bulk str".getBytes), 0) + val inputVector = BitVector( + "ing\r\n:100\r\n+A simple string\r\n*-1\r\n*2\r\n$8\r\nAnother1\r\n-An error\r\n*3\r\n$8\r\nAnother1\r\n*3\r\n*2\r\n+Simple string\r\n*2\r\n$3\r\nfoo\r\n-an error\r\n:13\r\n:12\r\n-An error\r\n*-1\r\n".getBytes) + nonEmptyFrame + .append(inputVector.toByteBuffer) + .fold( + err => fail(s"expected a result but failed with $err"), { + case r @ MoreThanOneFrame(_, _) => + r.complete shouldBe Vector( + CompleteFrame(BitVector("*3\r\n$16\r\nTest bulk string\r\n:100\r\n+A simple string\r\n".getBytes)), + CompleteFrame(BitVector("*-1\r\n".getBytes)), + CompleteFrame(BitVector("*2\r\n$8\r\nAnother1\r\n-An error\r\n".getBytes)), + CompleteFrame(BitVector( + "*3\r\n$8\r\nAnother1\r\n*3\r\n*2\r\n+Simple string\r\n*2\r\n$3\r\nfoo\r\n-an error\r\n:13\r\n:12\r\n-An error\r\n".getBytes)), + CompleteFrame(BitVector("*-1\r\n".getBytes)) + ) + case _ => fail(s"expected a MoreThanOne type") + } + ) + } + } + } +} diff --git a/core/src/test/scala/laserdisc/protocol/RESPFrameArraySpec.scala b/core/src/test/scala/laserdisc/protocol/RESPFrameArraySpec.scala deleted file mode 100644 index 6c5dda54..00000000 --- a/core/src/test/scala/laserdisc/protocol/RESPFrameArraySpec.scala +++ /dev/null @@ -1,154 +0,0 @@ -package laserdisc.protocol - -import laserdisc.protocol -import org.scalatest.{Matchers, WordSpecLike} -import scodec.bits.BitVector - -final class RESPFrameArraySpec extends WordSpecLike with Matchers { - - "An empty Array Frame" when { - - "appending a bit vector that represent an empty array" should { - "produce a complete frame with the bits of an empty bulk" in { - val inputVector = BitVector("*0\r\n".getBytes) - EmptyFrame.append(inputVector.toByteBuffer) should be(Right(CompleteFrame(inputVector))) - } - } - } - - "A non empty Array Frame" when { - - "appending a bit vector that completes it" should { - "produce a complete frame with the correct bits" in { - val nonEmptyFrame = IncompleteFrame(BitVector("*1\r\n$16\r\nTest bulk str".getBytes), 0) - val inputVector = BitVector("ing\r\n".getBytes) - val expected = BitVector("*1\r\n$16\r\nTest bulk string\r\n".getBytes) - nonEmptyFrame.append(inputVector.toByteBuffer) should be(Right(CompleteFrame(expected))) - } - } - - "appending a bit vector that doesn't complete the array but has complete objects" should { - "produce an incomplete frame with the correct partial and 0 as missing count" in { - val nonEmptyFrame = IncompleteFrame(BitVector("*3\r\n$16\r\nTest bulk str".getBytes), 0) - val inputVector = BitVector("ing\r\n:100\r\n".getBytes) - val expected = BitVector("*3\r\n$16\r\nTest bulk string\r\n:100\r\n".getBytes) - nonEmptyFrame.append(inputVector.toByteBuffer) should be(Right(IncompleteFrame(expected, 0))) - } - } - - "appending a bit vector that completes the array" should { - "produce a complete frame with the correct bits" in { - val nonEmptyFrame = IncompleteFrame(BitVector("*3\r\n$16\r\nTest bulk str".getBytes), 0) - val inputVector = BitVector("ing\r\n:100\r\n+A simple string\r\n".getBytes) - val expected = BitVector("*3\r\n$16\r\nTest bulk string\r\n:100\r\n+A simple string\r\n".getBytes) - nonEmptyFrame.append(inputVector.toByteBuffer) should be(Right(protocol.CompleteFrame(expected))) - } - } - - "appending a bit vector with more than one array" should { - "produce more than one frame with a list of the complete ones and an empty remainder" in { - val nonEmptyFrame = IncompleteFrame(BitVector("*3\r\n$16\r\nTest bulk str".getBytes), 0) - val inputVector = BitVector("ing\r\n:100\r\n+A simple string\r\n*2\r\n$8\r\nAnother1\r\n-An error\r\n".getBytes) - nonEmptyFrame.append(inputVector.toByteBuffer) should be( - Right( MoreThanOneFrame( - CompleteFrame(BitVector("*2\r\n$8\r\nAnother1\r\n-An error\r\n".getBytes)) :: - CompleteFrame(BitVector("*3\r\n$16\r\nTest bulk string\r\n:100\r\n+A simple string\r\n".getBytes)) :: Nil, - BitVector.empty - ) ) - ) - } - } - - "appending a bit vector with more than one array plus a reminder" should { - "produce more than one frame with a list of the complete ones and the correct remainder" in { - val nonEmptyFrame = IncompleteFrame(BitVector("*3\r\n$16\r\nTest bulk str".getBytes), 0) - val inputVector = BitVector("ing\r\n:100\r\n+A simple string\r\n*2\r\n$8\r\nAnother1\r\n-An error\r\n$17\r\nAnother bulk ".getBytes) - nonEmptyFrame.append(inputVector.toByteBuffer) should be( - Right( MoreThanOneFrame( - CompleteFrame(BitVector("*2\r\n$8\r\nAnother1\r\n-An error\r\n".getBytes)) :: - CompleteFrame(BitVector("*3\r\n$16\r\nTest bulk string\r\n:100\r\n+A simple string\r\n".getBytes)) :: Nil, - BitVector("$17\r\nAnother bulk ".getBytes()) - ) ) - ) - } - } - - "appending a bit vector with multiple null arrays" should { - "produce more than one frame with a list of the complete ones and the correct remainder" in { - val nonEmptyFrame = IncompleteFrame(BitVector("*3\r\n$16\r\nTest bulk str".getBytes), 0) - val inputVector = BitVector("ing\r\n:100\r\n+A simple string\r\n*-1\r\n*-1\r\n*-1\r\n*-1\r\n*-1\r\n".getBytes) - nonEmptyFrame.append(inputVector.toByteBuffer) should be( - Right( MoreThanOneFrame( - CompleteFrame(BitVector("*-1\r\n".getBytes)) :: - CompleteFrame(BitVector("*-1\r\n".getBytes)) :: - CompleteFrame(BitVector("*-1\r\n".getBytes)) :: - CompleteFrame(BitVector("*-1\r\n".getBytes)) :: - CompleteFrame(BitVector("*-1\r\n".getBytes)) :: - CompleteFrame(BitVector("*3\r\n$16\r\nTest bulk string\r\n:100\r\n+A simple string\r\n".getBytes)) :: Nil, - BitVector.empty - ) ) - ) - } - } - - "appending a bit vector with multiple null arrays the last of which not complete" should { - "produce more than one frame with a list of the complete ones and the correct remainder" in { - val nonEmptyFrame = IncompleteFrame(BitVector("*3\r\n$16\r\nTest bulk str".getBytes), 0) - val inputVector = BitVector("ing\r\n:100\r\n+A simple string\r\n*-1\r\n*-1\r\n*-1\r\n*-1\r\n*-1\r\n*".getBytes) - nonEmptyFrame.append(inputVector.toByteBuffer) should be( - Right( MoreThanOneFrame( - CompleteFrame(BitVector("*-1\r\n".getBytes)) :: - CompleteFrame(BitVector("*-1\r\n".getBytes)) :: - CompleteFrame(BitVector("*-1\r\n".getBytes)) :: - CompleteFrame(BitVector("*-1\r\n".getBytes)) :: - CompleteFrame(BitVector("*-1\r\n".getBytes)) :: - CompleteFrame(BitVector("*3\r\n$16\r\nTest bulk string\r\n:100\r\n+A simple string\r\n".getBytes)) :: Nil, - BitVector("*".getBytes()) - ) ) - ) - } - } - - "appending a bit vector with multiple arrays interleaved with null arrays" should { - "produce as result of `complete` more than one frame with a list of the complete arrays in the correct order" in { - val nonEmptyFrame = IncompleteFrame(BitVector("*3\r\n$16\r\nTest bulk str".getBytes), 0) - val inputVector = BitVector("ing\r\n:100\r\n+A simple string\r\n*-1\r\n*-1\r\n*2\r\n$8\r\nAnother1\r\n-An error\r\n*-1\r\n*-1\r\n*-1\r\n".getBytes) - nonEmptyFrame.append(inputVector.toByteBuffer).fold( - err => fail(s"expected a result but failed with $err"), - { - case r@MoreThanOneFrame(_, _) => r.complete shouldBe Vector( - CompleteFrame(BitVector("*3\r\n$16\r\nTest bulk string\r\n:100\r\n+A simple string\r\n".getBytes)), - CompleteFrame(BitVector("*-1\r\n".getBytes)), - CompleteFrame(BitVector("*-1\r\n".getBytes)), - CompleteFrame(BitVector("*2\r\n$8\r\nAnother1\r\n-An error\r\n".getBytes)), - CompleteFrame(BitVector("*-1\r\n".getBytes)), - CompleteFrame(BitVector("*-1\r\n".getBytes)), - CompleteFrame(BitVector("*-1\r\n".getBytes)) - ) - case _ => fail(s"expected a MoreThanOne type") - } - ) - } - } - - "appending a bit vector with multiple arrays containing nested arrays" should { - "produce as result of `complete` more than one frame with a list of the complete arrays in the correct order" in { - val nonEmptyFrame = IncompleteFrame(BitVector("*3\r\n$16\r\nTest bulk str".getBytes), 0) - val inputVector = BitVector("ing\r\n:100\r\n+A simple string\r\n*-1\r\n*2\r\n$8\r\nAnother1\r\n-An error\r\n*3\r\n$8\r\nAnother1\r\n*3\r\n*2\r\n+Simple string\r\n*2\r\n$3\r\nfoo\r\n-an error\r\n:13\r\n:12\r\n-An error\r\n*-1\r\n".getBytes) - nonEmptyFrame.append(inputVector.toByteBuffer).fold( - err => fail(s"expected a result but failed with $err"), - { - case r@MoreThanOneFrame(_, _) => r.complete shouldBe Vector( - CompleteFrame(BitVector("*3\r\n$16\r\nTest bulk string\r\n:100\r\n+A simple string\r\n".getBytes)), - CompleteFrame(BitVector("*-1\r\n".getBytes)), - CompleteFrame(BitVector("*2\r\n$8\r\nAnother1\r\n-An error\r\n".getBytes)), - CompleteFrame(BitVector("*3\r\n$8\r\nAnother1\r\n*3\r\n*2\r\n+Simple string\r\n*2\r\n$3\r\nfoo\r\n-an error\r\n:13\r\n:12\r\n-An error\r\n".getBytes)), - CompleteFrame(BitVector("*-1\r\n".getBytes)) - ) - case _ => fail(s"expected a MoreThanOne type") - } - ) - } - } - } -} diff --git a/core/src/test/scala/laserdisc/protocol/RESPFrameBulkStringSpec.scala b/core/src/test/scala/laserdisc/protocol/RESPFrameBulkStringSpec.scala index aa0376be..65611158 100644 --- a/core/src/test/scala/laserdisc/protocol/RESPFrameBulkStringSpec.scala +++ b/core/src/test/scala/laserdisc/protocol/RESPFrameBulkStringSpec.scala @@ -1,11 +1,11 @@ -package laserdisc.protocol +package laserdisc +package protocol -import org.scalatest.{Matchers, WordSpecLike} import scodec.bits.BitVector -final class RESPFrameBulkStringSpec extends WordSpecLike with Matchers { +final class RESPFrameBulkSpec extends BaseSpec { - "An empty BulkString Frame" when { + "An empty Bulk Frame" when { "appending a bit vector that's complete" should { "produce Complete with all the bits" in { @@ -32,10 +32,11 @@ final class RESPFrameBulkStringSpec extends WordSpecLike with Matchers { "produce MoreThanOne with a list of the complete ones and an empty remainder" in { val inputVector = BitVector("$16\r\nTest bulk string\r\n$16\r\nTest bulk string\r\n$16\r\nTest bulk string\r\n".getBytes) EmptyFrame.append(inputVector.toByteBuffer) should be( - Right( MoreThanOneFrame( - List.fill(3)(CompleteFrame(BitVector("$16\r\nTest bulk string\r\n".getBytes()))), - BitVector.empty - ) ) + Right( + MoreThanOneFrame( + List.fill(3)(CompleteFrame(BitVector("$16\r\nTest bulk string\r\n".getBytes()))), + BitVector.empty + )) ) } } @@ -44,10 +45,11 @@ final class RESPFrameBulkStringSpec extends WordSpecLike with Matchers { "produce MoreThanOne with a list of the complete ones and a remainder with the incomplete bits" in { val inputVector = BitVector("$16\r\nTest bulk string\r\n$16\r\nTest bulk string\r\n$16\r\nTest bulk".getBytes) EmptyFrame.append(inputVector.toByteBuffer) should be( - Right( MoreThanOneFrame( - List.fill(2)(CompleteFrame(BitVector("$16\r\nTest bulk string\r\n".getBytes()))), - BitVector("$16\r\nTest bulk".getBytes()) - ) ) + Right( + MoreThanOneFrame( + List.fill(2)(CompleteFrame(BitVector("$16\r\nTest bulk string\r\n".getBytes()))), + BitVector("$16\r\nTest bulk".getBytes()) + )) ) } } @@ -56,10 +58,11 @@ final class RESPFrameBulkStringSpec extends WordSpecLike with Matchers { "produce MoreThanOne with a list of the complete ones and an empty remainder" in { val inputVector = BitVector("$-1\r\n$-1\r\n$-1\r\n$-1\r\n$-1\r\n$-1\r\n".getBytes) EmptyFrame.append(inputVector.toByteBuffer) should be( - Right( MoreThanOneFrame( - List.fill(6)(CompleteFrame(BitVector("$-1\r\n".getBytes()))), - BitVector.empty - ) ) + Right( + MoreThanOneFrame( + List.fill(6)(CompleteFrame(BitVector("$-1\r\n".getBytes()))), + BitVector.empty + )) ) } } @@ -68,10 +71,11 @@ final class RESPFrameBulkStringSpec extends WordSpecLike with Matchers { "produce MoreThanOne with a list of the complete ones and a remainder with the incomplete bits" in { val inputVector = BitVector("$-1\r\n$-1\r\n$-1\r\n$-1\r\n$".getBytes) EmptyFrame.append(inputVector.toByteBuffer) should be( - Right( MoreThanOneFrame( - List.fill(4)(CompleteFrame(BitVector("$-1\r\n".getBytes()))), - BitVector("$".getBytes()) - ) ) + Right( + MoreThanOneFrame( + List.fill(4)(CompleteFrame(BitVector("$-1\r\n".getBytes()))), + BitVector("$".getBytes()) + )) ) } } @@ -79,44 +83,49 @@ final class RESPFrameBulkStringSpec extends WordSpecLike with Matchers { "appending a bit vector with multiple different messages with the last not complete" should { "produce MoreThanOne with a list of the complete ones in the inverted order and a remainder with the incomplete bits" in { - val inputVector = BitVector("$18\r\nTest bulk string 1\r\n$18\r\nTest bulk string 2\r\n$18\r\nTest bulk string 3\r\n$18\r\nTest bulk string 4\r\n$18\r\nTest bulk".getBytes) + val inputVector = BitVector( + "$18\r\nTest bulk string 1\r\n$18\r\nTest bulk string 2\r\n$18\r\nTest bulk string 3\r\n$18\r\nTest bulk string 4\r\n$18\r\nTest bulk".getBytes) EmptyFrame.append(inputVector.toByteBuffer) should be( - Right( MoreThanOneFrame( - CompleteFrame(BitVector("$18\r\nTest bulk string 4\r\n".getBytes())) :: - CompleteFrame(BitVector("$18\r\nTest bulk string 3\r\n".getBytes())) :: - CompleteFrame(BitVector("$18\r\nTest bulk string 2\r\n".getBytes())) :: - CompleteFrame(BitVector("$18\r\nTest bulk string 1\r\n".getBytes())) :: Nil, - BitVector("$18\r\nTest bulk".getBytes()) - ) ) + Right( + MoreThanOneFrame( + CompleteFrame(BitVector("$18\r\nTest bulk string 4\r\n".getBytes())) :: + CompleteFrame(BitVector("$18\r\nTest bulk string 3\r\n".getBytes())) :: + CompleteFrame(BitVector("$18\r\nTest bulk string 2\r\n".getBytes())) :: + CompleteFrame(BitVector("$18\r\nTest bulk string 1\r\n".getBytes())) :: Nil, + BitVector("$18\r\nTest bulk".getBytes()) + )) ) } "produce MoreThanOne where the call to complete should give a vector with the complete ones in the original order" in { - val inputVector = BitVector("$18\r\nTest bulk string 1\r\n$18\r\nTest bulk string 2\r\n$18\r\nTest bulk string 3\r\n$18\r\nTest bulk string 4\r\n$18\r\nTest bulk".getBytes) - EmptyFrame.append(inputVector.toByteBuffer).fold( - err => fail(s"expected a result but failed with $err"), - { - case r@MoreThanOneFrame(_, _) => r.complete shouldBe Vector( - CompleteFrame(BitVector("$18\r\nTest bulk string 1\r\n".getBytes())), - CompleteFrame(BitVector("$18\r\nTest bulk string 2\r\n".getBytes())), - CompleteFrame(BitVector("$18\r\nTest bulk string 3\r\n".getBytes())), - CompleteFrame(BitVector("$18\r\nTest bulk string 4\r\n".getBytes())) - ) - case _ => fail(s"expected a MoreThanOne type") - } - ) + val inputVector = BitVector( + "$18\r\nTest bulk string 1\r\n$18\r\nTest bulk string 2\r\n$18\r\nTest bulk string 3\r\n$18\r\nTest bulk string 4\r\n$18\r\nTest bulk".getBytes) + EmptyFrame + .append(inputVector.toByteBuffer) + .fold( + err => fail(s"expected a result but failed with $err"), { + case r @ MoreThanOneFrame(_, _) => + r.complete shouldBe Vector( + CompleteFrame(BitVector("$18\r\nTest bulk string 1\r\n".getBytes())), + CompleteFrame(BitVector("$18\r\nTest bulk string 2\r\n".getBytes())), + CompleteFrame(BitVector("$18\r\nTest bulk string 3\r\n".getBytes())), + CompleteFrame(BitVector("$18\r\nTest bulk string 4\r\n".getBytes())) + ) + case _ => fail(s"expected a MoreThanOne type") + } + ) } } } - "A non empty BulkString Frame" when { + "A non empty Bulk Frame" when { "appending a bit vector that completes it" should { "produce Complete with all the bits" in { val nonEmptyFrame = IncompleteFrame(BitVector("$16\r\nTest bulk str".getBytes), 0) - val inputVector = BitVector("ing\r\n".getBytes) - val expected = BitVector("$16\r\nTest bulk string\r\n".getBytes) + val inputVector = BitVector("ing\r\n".getBytes) + val expected = BitVector("$16\r\nTest bulk string\r\n".getBytes) nonEmptyFrame.append(inputVector.toByteBuffer) should be(Right(CompleteFrame(expected))) } } @@ -124,8 +133,8 @@ final class RESPFrameBulkStringSpec extends WordSpecLike with Matchers { "appending a bit vector that doesn't complete it" should { "produce Incomplete with the correct partial and the correct missing count" in { val nonEmptyFrame = IncompleteFrame(BitVector("$16\r\nTest bul".getBytes), 0) - val inputVector = BitVector("k str".getBytes) - val expected = BitVector("$16\r\nTest bulk str".getBytes) + val inputVector = BitVector("k str".getBytes) + val expected = BitVector("$16\r\nTest bulk str".getBytes) nonEmptyFrame.append(inputVector.toByteBuffer) should be(Right(IncompleteFrame(expected, 40))) } } @@ -133,12 +142,13 @@ final class RESPFrameBulkStringSpec extends WordSpecLike with Matchers { "appending a bit vector with multiple messages all complete" should { "produce MoreThanOne with a list of the complete ones and an empty remainder" in { val nonEmptyFrame = IncompleteFrame(BitVector("$16\r\nTest bulk s".getBytes), 0) - val inputVector = BitVector("tring\r\n$16\r\nTest bulk string\r\n$16\r\nTest bulk string\r\n".getBytes) + val inputVector = BitVector("tring\r\n$16\r\nTest bulk string\r\n$16\r\nTest bulk string\r\n".getBytes) nonEmptyFrame.append(inputVector.toByteBuffer) should be( - Right( MoreThanOneFrame( - List.fill(3)(CompleteFrame(BitVector("$16\r\nTest bulk string\r\n".getBytes()))), - BitVector.empty - ) ) + Right( + MoreThanOneFrame( + List.fill(3)(CompleteFrame(BitVector("$16\r\nTest bulk string\r\n".getBytes()))), + BitVector.empty + )) ) } } @@ -146,12 +156,13 @@ final class RESPFrameBulkStringSpec extends WordSpecLike with Matchers { "appending a bit vector with multiple messages with the last not complete" should { "produce MoreThanOne with a list of the complete ones and a remainder with the incomplete bits" in { val nonEmptyFrame = IncompleteFrame(BitVector("$16\r\nTest bulk s".getBytes), 0) - val inputVector = BitVector("tring\r\n$16\r\nTest bulk string\r\n$16\r\nTest bulk".getBytes) + val inputVector = BitVector("tring\r\n$16\r\nTest bulk string\r\n$16\r\nTest bulk".getBytes) nonEmptyFrame.append(inputVector.toByteBuffer) should be( - Right( MoreThanOneFrame( - List.fill(2)(CompleteFrame(BitVector("$16\r\nTest bulk string\r\n".getBytes()))), - BitVector("$16\r\nTest bulk".getBytes()) - ) ) + Right( + MoreThanOneFrame( + List.fill(2)(CompleteFrame(BitVector("$16\r\nTest bulk string\r\n".getBytes()))), + BitVector("$16\r\nTest bulk".getBytes()) + )) ) } } @@ -159,12 +170,13 @@ final class RESPFrameBulkStringSpec extends WordSpecLike with Matchers { "appending a bit vector with multiple null bulk all complete" should { "produce MoreThanOne with a list of the complete ones and an empty remainder" in { val nonEmptyFrame = IncompleteFrame(BitVector("$-".getBytes), 0) - val inputVector = BitVector("1\r\n$-1\r\n$-1\r\n$-1\r\n$-1\r\n$-1\r\n".getBytes) + val inputVector = BitVector("1\r\n$-1\r\n$-1\r\n$-1\r\n$-1\r\n$-1\r\n".getBytes) nonEmptyFrame.append(inputVector.toByteBuffer) should be( - Right( MoreThanOneFrame( - List.fill(6)(CompleteFrame(BitVector("$-1\r\n".getBytes()))), - BitVector.empty - ) ) + Right( + MoreThanOneFrame( + List.fill(6)(CompleteFrame(BitVector("$-1\r\n".getBytes()))), + BitVector.empty + )) ) } } @@ -172,12 +184,13 @@ final class RESPFrameBulkStringSpec extends WordSpecLike with Matchers { "appending a bit vector with multiple null bulk with the last not complete" should { "produce MoreThanOne with a list of the complete ones and a remainder with the incomplete bits" in { val nonEmptyFrame = IncompleteFrame(BitVector("$-".getBytes), 0) - val inputVector = BitVector("1\r\n$-1\r\n$-1\r\n$-1\r\n$".getBytes) + val inputVector = BitVector("1\r\n$-1\r\n$-1\r\n$-1\r\n$".getBytes) nonEmptyFrame.append(inputVector.toByteBuffer) should be( - Right( MoreThanOneFrame( - List.fill(4)(CompleteFrame(BitVector("$-1\r\n".getBytes()))), - BitVector("$".getBytes()) - ) ) + Right( + MoreThanOneFrame( + List.fill(4)(CompleteFrame(BitVector("$-1\r\n".getBytes()))), + BitVector("$".getBytes()) + )) ) } } @@ -186,33 +199,38 @@ final class RESPFrameBulkStringSpec extends WordSpecLike with Matchers { "produce MoreThanOne with a list of the complete ones in the inverted order and a remainder with the incomplete bits" in { val nonEmptyFrame = IncompleteFrame(BitVector("$21\r\nTest bulk s".getBytes), 0) - val inputVector = BitVector("tring 1 11\r\n$17\r\nTest bulk string2\r\n$20\r\nTest bulk string 3 1\r\n$19\r\nTest bulk string 40\r\n$18\r\nTest bulk".getBytes) + val inputVector = BitVector( + "tring 1 11\r\n$17\r\nTest bulk string2\r\n$20\r\nTest bulk string 3 1\r\n$19\r\nTest bulk string 40\r\n$18\r\nTest bulk".getBytes) nonEmptyFrame.append(inputVector.toByteBuffer) should be( - Right( MoreThanOneFrame( - CompleteFrame(BitVector("$19\r\nTest bulk string 40\r\n".getBytes())) :: - CompleteFrame(BitVector("$20\r\nTest bulk string 3 1\r\n".getBytes())) :: - CompleteFrame(BitVector("$17\r\nTest bulk string2\r\n".getBytes())) :: - CompleteFrame(BitVector("$21\r\nTest bulk string 1 11\r\n".getBytes())) :: Nil, - BitVector("$18\r\nTest bulk".getBytes()) - ) ) + Right( + MoreThanOneFrame( + CompleteFrame(BitVector("$19\r\nTest bulk string 40\r\n".getBytes())) :: + CompleteFrame(BitVector("$20\r\nTest bulk string 3 1\r\n".getBytes())) :: + CompleteFrame(BitVector("$17\r\nTest bulk string2\r\n".getBytes())) :: + CompleteFrame(BitVector("$21\r\nTest bulk string 1 11\r\n".getBytes())) :: Nil, + BitVector("$18\r\nTest bulk".getBytes()) + )) ) } "produce MoreThanOne where the call to complete should give a vector with the complete ones in the original order" in { val nonEmptyFrame = IncompleteFrame(BitVector("$21\r\nTest bulk s".getBytes), 0) - val inputVector = BitVector("tring 1 11\r\n$17\r\nTest bulk string2\r\n$20\r\nTest bulk string 3 1\r\n$19\r\nTest bulk string 40\r\n$18\r\nTest bulk".getBytes) - nonEmptyFrame.append(inputVector.toByteBuffer).fold( - err => fail(s"expected a result but failed with $err"), - { - case r@MoreThanOneFrame(_, _) => r.complete shouldBe Vector( - CompleteFrame(BitVector("$21\r\nTest bulk string 1 11\r\n".getBytes())), - CompleteFrame(BitVector("$17\r\nTest bulk string2\r\n".getBytes())), - CompleteFrame(BitVector("$20\r\nTest bulk string 3 1\r\n".getBytes())), - CompleteFrame(BitVector("$19\r\nTest bulk string 40\r\n".getBytes())) - ) - case _ => fail(s"expected a MoreThanOne type") - } - ) + val inputVector = BitVector( + "tring 1 11\r\n$17\r\nTest bulk string2\r\n$20\r\nTest bulk string 3 1\r\n$19\r\nTest bulk string 40\r\n$18\r\nTest bulk".getBytes) + nonEmptyFrame + .append(inputVector.toByteBuffer) + .fold( + err => fail(s"expected a result but failed with $err"), { + case r @ MoreThanOneFrame(_, _) => + r.complete shouldBe Vector( + CompleteFrame(BitVector("$21\r\nTest bulk string 1 11\r\n".getBytes())), + CompleteFrame(BitVector("$17\r\nTest bulk string2\r\n".getBytes())), + CompleteFrame(BitVector("$20\r\nTest bulk string 3 1\r\n".getBytes())), + CompleteFrame(BitVector("$19\r\nTest bulk string 40\r\n".getBytes())) + ) + case _ => fail(s"expected a MoreThanOne type") + } + ) } } } diff --git a/core/src/test/scala/laserdisc/protocol/RESPFrameMixedSpec.scala b/core/src/test/scala/laserdisc/protocol/RESPFrameMixedSpec.scala index 694bc29e..0b83a98a 100644 --- a/core/src/test/scala/laserdisc/protocol/RESPFrameMixedSpec.scala +++ b/core/src/test/scala/laserdisc/protocol/RESPFrameMixedSpec.scala @@ -1,68 +1,73 @@ -package laserdisc.protocol +package laserdisc +package protocol import eu.timepit.refined.types.string.NonEmptyString -import laserdisc.OneOrMore import org.scalacheck.{Arbitrary, Gen} -import org.scalatest.{Matchers, WordSpecLike} -import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks import scodec.bits.BitVector -final class RESPFrameMixedSpec extends WordSpecLike with Matchers with ScalaCheckPropertyChecks { +final class RESPFrameMixedSpec extends BaseSpec { "A non empty mixed Frame" when { "appending a bit vector composed of a complete sequence of integers, simple strings, bulk strings and errors" should { "produce MoreThanOne with a list of all the complete items" in { val nonEmptyFrame = IncompleteFrame(BitVector("$16\r\nTest bulk str".getBytes), 0) - val inputVector = BitVector("ing\r\n+OK\r\n$0\r\n\r\n+Another simple string\r\n*3\r\n$16\r\nTest bulk string\r\n:100\r\n+A simple string\r\n-Possible error message\r\n*0\r\n:1\r\n:2\r\n*2\r\n$8\r\nAnother1\r\n-An error\r\n:177\r\n+Another simple string\r\n$21\r\nTest bulk string 1 11\r\n*5\r\n$16\r\nTest bulk string\r\n:13\r\n-1234 An error with numbers\r\n:100\r\n+A simple string\r\n-And an error message\r\n".getBytes) - nonEmptyFrame.append(inputVector.toByteBuffer).fold( - err => fail(s"expected a result but failed with $err"), - { - case r@MoreThanOneFrame(_, _) => r.complete shouldBe Vector( - CompleteFrame(BitVector("$16\r\nTest bulk string\r\n".getBytes())), - CompleteFrame(BitVector("+OK\r\n".getBytes())), - CompleteFrame(BitVector("$0\r\n\r\n".getBytes())), - CompleteFrame(BitVector("+Another simple string\r\n".getBytes())), - CompleteFrame(BitVector("*3\r\n$16\r\nTest bulk string\r\n:100\r\n+A simple string\r\n".getBytes)), - CompleteFrame(BitVector("-Possible error message\r\n".getBytes())), - CompleteFrame(BitVector("*0\r\n".getBytes())), - CompleteFrame(BitVector(":1\r\n".getBytes())), - CompleteFrame(BitVector(":2\r\n".getBytes())), - CompleteFrame(BitVector("*2\r\n$8\r\nAnother1\r\n-An error\r\n".getBytes)), - CompleteFrame(BitVector(":177\r\n".getBytes())), - CompleteFrame(BitVector("+Another simple string\r\n".getBytes())), - CompleteFrame(BitVector("$21\r\nTest bulk string 1 11\r\n".getBytes())), - CompleteFrame(BitVector("*5\r\n$16\r\nTest bulk string\r\n:13\r\n-1234 An error with numbers\r\n:100\r\n+A simple string\r\n".getBytes)), - CompleteFrame(BitVector("-And an error message\r\n".getBytes())) - ) - case _ => fail(s"expected a MoreThanOne type") - } - ) + val inputVector = BitVector( + "ing\r\n+OK\r\n$0\r\n\r\n+Another simple string\r\n*3\r\n$16\r\nTest bulk string\r\n:100\r\n+A simple string\r\n-Possible error message\r\n*0\r\n:1\r\n:2\r\n*2\r\n$8\r\nAnother1\r\n-An error\r\n:177\r\n+Another simple string\r\n$21\r\nTest bulk string 1 11\r\n*5\r\n$16\r\nTest bulk string\r\n:13\r\n-1234 An error with numbers\r\n:100\r\n+A simple string\r\n-And an error message\r\n".getBytes) + nonEmptyFrame + .append(inputVector.toByteBuffer) + .fold( + err => fail(s"expected a result but failed with $err"), { + case r @ MoreThanOneFrame(_, _) => + r.complete shouldBe Vector( + CompleteFrame(BitVector("$16\r\nTest bulk string\r\n".getBytes())), + CompleteFrame(BitVector("+OK\r\n".getBytes())), + CompleteFrame(BitVector("$0\r\n\r\n".getBytes())), + CompleteFrame(BitVector("+Another simple string\r\n".getBytes())), + CompleteFrame(BitVector("*3\r\n$16\r\nTest bulk string\r\n:100\r\n+A simple string\r\n".getBytes)), + CompleteFrame(BitVector("-Possible error message\r\n".getBytes())), + CompleteFrame(BitVector("*0\r\n".getBytes())), + CompleteFrame(BitVector(":1\r\n".getBytes())), + CompleteFrame(BitVector(":2\r\n".getBytes())), + CompleteFrame(BitVector("*2\r\n$8\r\nAnother1\r\n-An error\r\n".getBytes)), + CompleteFrame(BitVector(":177\r\n".getBytes())), + CompleteFrame(BitVector("+Another simple string\r\n".getBytes())), + CompleteFrame(BitVector("$21\r\nTest bulk string 1 11\r\n".getBytes())), + CompleteFrame(BitVector( + "*5\r\n$16\r\nTest bulk string\r\n:13\r\n-1234 An error with numbers\r\n:100\r\n+A simple string\r\n".getBytes)), + CompleteFrame(BitVector("-And an error message\r\n".getBytes())) + ) + case _ => fail(s"expected a MoreThanOne type") + } + ) } } "appending a bit vector composed of sequence of integers, simple strings, bulk strings and errors that are not complete" should { "produce MoreThanOne with a list of all the complete items plus the remainder" in { val nonEmptyFrame = IncompleteFrame(BitVector("$16\r\nTest bulk str".getBytes), 0) - val inputVector = BitVector("ing\r\n+OK\r\n+Another simple string\r\n-Possible error message\r\n:1\r\n:2\r\n:177\r\n+Another simple string\r\n$21\r\nTest bulk string 1 11\r\n-And an error message\r\n".getBytes) - nonEmptyFrame.append(inputVector.toByteBuffer).fold( - err => fail(s"expected a result but failed with $err"), - { - case r@MoreThanOneFrame(_, _) => r.complete shouldBe Vector( - CompleteFrame(BitVector("$16\r\nTest bulk string\r\n".getBytes())), - CompleteFrame(BitVector("+OK\r\n".getBytes())), - CompleteFrame(BitVector("+Another simple string\r\n".getBytes())), - CompleteFrame(BitVector("-Possible error message\r\n".getBytes())), - CompleteFrame(BitVector(":1\r\n".getBytes())), - CompleteFrame(BitVector(":2\r\n".getBytes())), - CompleteFrame(BitVector(":177\r\n".getBytes())), - CompleteFrame(BitVector("+Another simple string\r\n".getBytes())), - CompleteFrame(BitVector("$21\r\nTest bulk string 1 11\r\n".getBytes())), - CompleteFrame(BitVector("-And an error message\r\n".getBytes())) - ) - case _ => fail(s"expected a MoreThanOne type") - } - ) + val inputVector = BitVector( + "ing\r\n+OK\r\n+Another simple string\r\n-Possible error message\r\n:1\r\n:2\r\n:177\r\n+Another simple string\r\n$21\r\nTest bulk string 1 11\r\n-And an error message\r\n".getBytes) + nonEmptyFrame + .append(inputVector.toByteBuffer) + .fold( + err => fail(s"expected a result but failed with $err"), { + case r @ MoreThanOneFrame(_, _) => + r.complete shouldBe Vector( + CompleteFrame(BitVector("$16\r\nTest bulk string\r\n".getBytes())), + CompleteFrame(BitVector("+OK\r\n".getBytes())), + CompleteFrame(BitVector("+Another simple string\r\n".getBytes())), + CompleteFrame(BitVector("-Possible error message\r\n".getBytes())), + CompleteFrame(BitVector(":1\r\n".getBytes())), + CompleteFrame(BitVector(":2\r\n".getBytes())), + CompleteFrame(BitVector(":177\r\n".getBytes())), + CompleteFrame(BitVector("+Another simple string\r\n".getBytes())), + CompleteFrame(BitVector("$21\r\nTest bulk string 1 11\r\n".getBytes())), + CompleteFrame(BitVector("-And an error message\r\n".getBytes())) + ) + case _ => fail(s"expected a MoreThanOne type") + } + ) } } @@ -72,18 +77,18 @@ final class RESPFrameMixedSpec extends WordSpecLike with Matchers with ScalaChec "appending a random sequence of complete messages" should { "produce MoreThanOne with all the complete items" in { forAll { testSet: OneOrMore[String] => + val vector = BitVector(testSet.value.mkString.getBytes()) - val vector = BitVector(testSet.value.mkString. getBytes()) - - EmptyFrame.append(vector.toByteBuffer).fold( - err => fail(s"expected a result but failed with $err"), - { - case r@MoreThanOneFrame(_, _) => - r.complete.size shouldBe testSet.value.size - r.remainder should be (empty) - case _ => fail(s"expected a MoreThanOne type") - } - ) + EmptyFrame + .append(vector.toByteBuffer) + .fold( + err => fail(s"expected a result but failed with $err"), { + case r @ MoreThanOneFrame(_, _) => + r.complete.size shouldBe testSet.value.size + r.remainder should be(empty) + case _ => fail(s"expected a MoreThanOne type") + } + ) } } } @@ -101,38 +106,43 @@ final class RESPFrameMixedSpec extends WordSpecLike with Matchers with ScalaChec private[this] implicit def arbitraryMessages(implicit ev1: Arbitrary[Int], ev2: Arbitrary[NonEmptyString]): Arbitrary[OneOrMore[String]] = Arbitrary { (for { - n <- Gen.choose(2, 1000) - i <- ev1.arbitrary map (n => s":$n\r\n") - s <- ev2.arbitrary map (st => s"+$st\r\n") - e <- ev2.arbitrary map (st => s"-$st\r\n") - xs <- Gen.listOfN(n, Gen.oneOf( - Seq( - i, s, e, - "$16\r\nTest bulk string\r\n", - "+OK\r\n", - "+a\r\n", - "+Another simple string\r\n", - "-Possible error message\r\n", - "-1234 Error with numbers\r\n", - ":1\r\n", - ":2\r\n", - ":177\r\n", - ":123456789\r\n", - ":-1\r\n", - "+Very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long single line string\r\n", - "+Another simple string\r\n", - "$21\r\nTest bulk string 1 11\r\n", - "$-1\r\n", - "$0\r\n\r\n", - "-And an error message\r\n", - "$21\r\nTest bulk string 1 11\r\n", - "*5\r\n$16\r\nTest bulk string\r\n:13\r\n-1234 An error with numbers\r\n:100\r\n+A simple string\r\n", - "*3\r\n$16\r\nTest bulk string\r\n:100\r\n+A simple string\r\n", - "*2\r\n$8\r\nAnother1\r\n-An error\r\n", - "*0\r\n", - "*-1\r\n" - ) - )) + n <- Gen.choose(2, 1000) + i <- ev1.arbitrary map (n => s":$n\r\n") + s <- ev2.arbitrary map (st => s"+$st\r\n") + e <- ev2.arbitrary map (st => s"-$st\r\n") + xs <- Gen.listOfN( + n, + Gen.oneOf( + Seq( + i, + s, + e, + "$16\r\nTest bulk string\r\n", + "+OK\r\n", + "+a\r\n", + "+Another simple string\r\n", + "-Possible error message\r\n", + "-1234 Error with numbers\r\n", + ":1\r\n", + ":2\r\n", + ":177\r\n", + ":123456789\r\n", + ":-1\r\n", + "+Very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long single line string\r\n", + "+Another simple string\r\n", + "$21\r\nTest bulk string 1 11\r\n", + "$-1\r\n", + "$0\r\n\r\n", + "-And an error message\r\n", + "$21\r\nTest bulk string 1 11\r\n", + "*5\r\n$16\r\nTest bulk string\r\n:13\r\n-1234 An error with numbers\r\n:100\r\n+A simple string\r\n", + "*3\r\n$16\r\nTest bulk string\r\n:100\r\n+A simple string\r\n", + "*2\r\n$8\r\nAnother1\r\n-An error\r\n", + "*0\r\n", + "*-1\r\n" + ) + ) + ) } yield xs) map (xs => OneOrMore.unsafeFrom(xs)) } } diff --git a/core/src/test/scala/laserdisc/protocol/RESPFunctionsSpec.scala b/core/src/test/scala/laserdisc/protocol/RESPFunctionsSpec.scala index 2073ec82..d96488ea 100644 --- a/core/src/test/scala/laserdisc/protocol/RESPFunctionsSpec.scala +++ b/core/src/test/scala/laserdisc/protocol/RESPFunctionsSpec.scala @@ -1,78 +1,81 @@ -package laserdisc.protocol +package laserdisc +package protocol -import laserdisc.protocol.BitVectorDecoding.{Complete, CompleteWithRemainder, Incomplete, MissingBits} -import org.scalatest.{Matchers, WordSpecLike} +import BitVectorDecoding.{Complete, CompleteWithRemainder, Incomplete, MissingBits} import scodec.bits.BitVector -final class RESPFunctionsSpec extends WordSpecLike with Matchers { +final class RESPFunctionsSpec extends BaseSpec { "A RESP codec" when { "checking the state of a bit vector with the size prefix not complete" should { "produce IncompleteVector" in { - RESP.stateOf(BitVector("$2362".getBytes)) should be (Right(Incomplete)) + RESP.stateOf(BitVector("$2362".getBytes)) should be(Right(Incomplete)) } } "checking the state of a bit vector with only the data type selector" should { "produce IncompleteVector" in { - RESP.stateOf(BitVector("$".getBytes)) should be (Right(Incomplete)) + RESP.stateOf(BitVector("$".getBytes)) should be(Right(Incomplete)) } } "checking the state of a bit vector that's complete" should { "produce CompleteVector" in { - RESP.stateOf(BitVector("$16\r\nTest bulk string\r\n".getBytes)) should be (Right(Complete)) + RESP.stateOf(BitVector("$16\r\nTest bulk string\r\n".getBytes)) should be(Right(Complete)) } } "checking the state of a bit vector whit the size prefix complete and an incomplete payload" should { "produce MissingBits with the correct number of bits missing" in { - RESP.stateOf(BitVector("$40\r\nIncomplete test bulk string".getBytes)) should be (Right(MissingBits(120))) + RESP.stateOf(BitVector("$40\r\nIncomplete test bulk string".getBytes)) should be(Right(MissingBits(120))) } } "checking the state of a bit vector that represents an empty bulk" should { "produce CompleteVector" in { - RESP.stateOf(BitVector("$-1\r\n".getBytes)) should be (Right(Complete)) + RESP.stateOf(BitVector("$-1\r\n".getBytes)) should be(Right(Complete)) } } "checking the state of an incomplete bit vector that represents an empty bulk" should { "produce MissingBits" in { - RESP.stateOf(BitVector("$-".getBytes)) should be (Right(Incomplete)) + RESP.stateOf(BitVector("$-".getBytes)) should be(Right(Incomplete)) } } "checking the state of a bulk bit vector that contains one message complete and one not complete" should { "produce CompleteWithRemainder" in { - RESP.stateOf(BitVector("$16\r\nTest bulk string\r\n$16\r\nTest bulk".getBytes)) should be ( - Right(CompleteWithRemainder( - BitVector("$16\r\nTest bulk string\r\n".toCharArray.map(_.toByte)), - BitVector("$16\r\nTest bulk".toCharArray.map(_.toByte)) - )) + RESP.stateOf(BitVector("$16\r\nTest bulk string\r\n$16\r\nTest bulk".getBytes)) should be( + Right( + CompleteWithRemainder( + BitVector("$16\r\nTest bulk string\r\n".toCharArray.map(_.toByte)), + BitVector("$16\r\nTest bulk".toCharArray.map(_.toByte)) + )) ) } } "checking the state of a bulk bit vector that contains more than one complete messages" should { "produce CompleteWithRemainder" in { - RESP.stateOf(BitVector("$16\r\nTest bulk string\r\n$16\r\nTest bulk string\r\n$16\r\nTest bulk string\r\n".getBytes)) should be ( - Right(CompleteWithRemainder( - BitVector("$16\r\nTest bulk string\r\n".toCharArray.map(_.toByte)), - BitVector("$16\r\nTest bulk string\r\n$16\r\nTest bulk string\r\n".toCharArray.map(_.toByte)) - )) + RESP.stateOf(BitVector("$16\r\nTest bulk string\r\n$16\r\nTest bulk string\r\n$16\r\nTest bulk string\r\n".getBytes)) should be( + Right( + CompleteWithRemainder( + BitVector("$16\r\nTest bulk string\r\n".toCharArray.map(_.toByte)), + BitVector("$16\r\nTest bulk string\r\n$16\r\nTest bulk string\r\n".toCharArray.map(_.toByte)) + )) ) } } "checking the state of a bulk bit vector that contains more than one null message" should { "produce CompleteWithRemainder" in { - RESP.stateOf(BitVector("$-1\r\n$-1\r\n$-1\r\n".getBytes)) should be ( - Right(CompleteWithRemainder( - BitVector("$-1\r\n".toCharArray.map(_.toByte)), - BitVector("$-1\r\n$-1\r\n".toCharArray.map(_.toByte)) - )) + RESP.stateOf(BitVector("$-1\r\n$-1\r\n$-1\r\n".getBytes)) should be( + Right( + CompleteWithRemainder( + BitVector("$-1\r\n".toCharArray.map(_.toByte)), + BitVector("$-1\r\n$-1\r\n".toCharArray.map(_.toByte)) + )) ) } } diff --git a/core/src/test/scala/laserdisc/types.scala b/core/src/test/scala/laserdisc/types.scala new file mode 100644 index 00000000..a98e837d --- /dev/null +++ b/core/src/test/scala/laserdisc/types.scala @@ -0,0 +1,21 @@ +package laserdisc + +import org.scalacheck.Arbitrary +import org.scalacheck.Arbitrary.arbitrary + +final case class Foo(x: Int) +object Foo { + implicit final val fooRead: Bulk ==> Foo = Read.instancePF { case Bulk(ToInt(i)) => Foo(i) } +} + +final case class Bar(x: String) + +final case class Baz(f1: Int, f2: String) +object Baz { + implicit final val bazArb: Arbitrary[Baz] = Arbitrary { + for { + i <- arbitrary[Int] + s <- arbitrary[String] + } yield Baz(i, s) + } +} diff --git a/fs2/src/main/scala-2.11/laserdisc/fs2/fs2.scala b/fs2/src/main/scala-2.11/laserdisc/fs2/fs2.scala deleted file mode 100644 index 07d09b4a..00000000 --- a/fs2/src/main/scala-2.11/laserdisc/fs2/fs2.scala +++ /dev/null @@ -1,21 +0,0 @@ -package laserdisc - -import scala.util.{Failure, Success, Try} - -package object fs2 { - final type Pipe[F[_], -I, +O] = _root_.fs2.Pipe[F, I, O] - final type Queue[F[_]] = _root_.fs2.concurrent.Queue[F, Request[F]] - final type Signal[F[_], A] = _root_.fs2.concurrent.SignallingRef[F, A] - final type Stream[+F[_], +O] = _root_.fs2.Stream[F, O] - - final val Queue = _root_.fs2.concurrent.Queue - final val Signal = _root_.fs2.concurrent.SignallingRef - final val Stream = _root_.fs2.Stream - - implicit final class RichTry[+T](val self: Try[T]) extends AnyVal { - @inline def toEither: Throwable | T = self match { - case Success(t) => Right(t) - case Failure(t) => Left(t) - } - } -} diff --git a/fs2/src/main/scala/laserdisc/fs2/ProtocolHandler.scala b/fs2/src/main/scala/laserdisc/fs2/ProtocolHandler.scala index 46806b9e..4353e2d1 100644 --- a/fs2/src/main/scala/laserdisc/fs2/ProtocolHandler.scala +++ b/fs2/src/main/scala/laserdisc/fs2/ProtocolHandler.scala @@ -16,11 +16,12 @@ sealed trait ProtocolHandler[F[_], In <: HList] extends DepFn2[In, (Queue[F], Fi object ProtocolHandler { @implicitNotFound( - "Cannot derive ProtocolHandler.\n\nThis usually depends on:\n" + + "Cannot derive ProtocolHandler.\n\n" + + "This usually depends on:\n" + " - Not having an HList of only Protocols\n" + " - Not having available in implicit scope an instance of a cats.effect.Concurrent for the chosen F\n" + - " - Not having available in implicit scope an instance of a cats.effect.Timer for the chosen F") - type Aux[F[_], In <: HList, LOut0 <: HList] = ProtocolHandler[F, In] { type LOut = LOut0 } + " - Not having available in implicit scope an instance of a cats.effect.Timer for the chosen F" + ) type Aux[F[_], In <: HList, LOut0 <: HList] = ProtocolHandler[F, In] { type LOut = LOut0 } implicit final def derive[ F[_]: Concurrent: Timer, diff --git a/fs2/src/main/scala/laserdisc/fs2/RedisClient.scala b/fs2/src/main/scala/laserdisc/fs2/RedisClient.scala index e83965d0..087402f0 100644 --- a/fs2/src/main/scala/laserdisc/fs2/RedisClient.scala +++ b/fs2/src/main/scala/laserdisc/fs2/RedisClient.scala @@ -213,8 +213,7 @@ object RedisClient { } } - def runner(knownServer: Stream[F, Option[RedisAddress]], - lastFailedServer: Option[RedisAddress] = None): Stream[F, Unit] = + def runner(knownServer: Stream[F, Option[RedisAddress]], lastFailedServer: Option[RedisAddress] = None): Stream[F, Unit] = knownServer.flatMap { case None => serverUnavailable.flatMap(address => runner(Stream.emit(Some(address)))) diff --git a/fs2/src/main/scala/laserdisc/fs2/Request.scala b/fs2/src/main/scala/laserdisc/fs2/Request.scala index d1f1c4cc..524f227b 100644 --- a/fs2/src/main/scala/laserdisc/fs2/Request.scala +++ b/fs2/src/main/scala/laserdisc/fs2/Request.scala @@ -8,8 +8,9 @@ sealed trait Request[F[_]] { } object Request { - sealed abstract case class Req[F[_], A0](protocol: Protocol.Aux[A0], callback: Maybe[A0] => F[Unit]) - extends Request[F] { override type A = A0 } + sealed abstract case class Req[F[_], A0](protocol: Protocol.Aux[A0], callback: Maybe[A0] => F[Unit]) extends Request[F] { + override type A = A0 + } def apply[F[_], A](protocol: Protocol.Aux[A], callback: Maybe[A] => F[Unit]): Request[F] = new Req[F, A](protocol, callback) {} diff --git a/fs2/src/main/scala/laserdisc/fs2/exceptions.scala b/fs2/src/main/scala/laserdisc/fs2/exceptions.scala index 8a40fee8..d836b1b2 100644 --- a/fs2/src/main/scala/laserdisc/fs2/exceptions.scala +++ b/fs2/src/main/scala/laserdisc/fs2/exceptions.scala @@ -3,9 +3,7 @@ package fs2 final case class ServerTerminatedConnection(redisAddress: RedisAddress) extends Platform.LaserDiscRuntimeError(s"Server $redisAddress terminated client connection") -final object ServerUnavailable extends Platform.LaserDiscRuntimeError("No server available") -final object ClientTerminated extends Platform.LaserDiscRuntimeError("Client terminated connection") -final case class NoInFlightRequest(resp: RESP) - extends Platform.LaserDiscRuntimeError(s"Got unsolicited message from server: $resp") -final case class RequestTimedOut[A](protocol: Protocol.Aux[A]) - extends Platform.LaserDiscRuntimeError(s"The request $protocol timed-out") +final object ServerUnavailable extends Platform.LaserDiscRuntimeError("No server available") +final object ClientTerminated extends Platform.LaserDiscRuntimeError("Client terminated connection") +final case class NoInFlightRequest(resp: RESP) extends Platform.LaserDiscRuntimeError(s"Got unsolicited message from server: $resp") +final case class RequestTimedOut[A](protocol: Protocol.Aux[A]) extends Platform.LaserDiscRuntimeError(s"The request $protocol timed-out") diff --git a/fs2/src/main/scala-2.12/laserdisc/fs2/fs2.scala b/fs2/src/main/scala/laserdisc/fs2/fs2.scala similarity index 100% rename from fs2/src/main/scala-2.12/laserdisc/fs2/fs2.scala rename to fs2/src/main/scala/laserdisc/fs2/fs2.scala diff --git a/fs2/src/test/scala/laserdisc/fs2/RedisClientSpec.scala b/fs2/src/test/scala/laserdisc/fs2/RedisClientSpec.scala index a121c607..092c93ad 100644 --- a/fs2/src/test/scala/laserdisc/fs2/RedisClientSpec.scala +++ b/fs2/src/test/scala/laserdisc/fs2/RedisClientSpec.scala @@ -12,7 +12,7 @@ import cats.syntax.apply._ import cats.syntax.flatMap._ import cats.syntax.functor._ import cats.syntax.traverse._ -import eu.timepit.refined.auto._ +import laserdisc.auto._ import log.effect.fs2.Fs2LogWriter.noOpLogStreamF import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike} @@ -183,7 +183,7 @@ final class RedisClientSpec extends WordSpecLike with Matchers with BeforeAndAft "handle correctly hundreds of read requests in parallel for an array payload" in { implicit object myShow1 extends Show[List[String]] { - final def show(a: List[String]): String = a mkString "," + final def show(a: List[String]): String = a mkString COMMA } val testKey = Key("test-key-list")