## Motivation
Since Redis 6.0, a new serialization protocol format (v3) is available that gives richer semantic reasoning behind the different types to enable commands to better understand the return types to provide in their programming language.
In addition, the `RESPTranslator` type is going to see more direct usage, and the current API doesn't make read well.
## Changes
- Add: Internal `RESPVersion` enum that the `RESPTranslator` will start to use
- Rename: `RESPTranslator.parseBytes` to `RESPTranslator.read(from:)`
Motivation:
RediStack today represents a command as a temporary object for the purpose of writing to the channel.
While it is useful to have an object for that purpose, commands handled in this way require immediate execution
and aren't available for other purposes.
Commands can serve a better purpose as a lightweight object to support delayed execution,
so that pipeling as described in issue #63 could be possible.
Modifications:
- Add: `get` overloads for JSON codable interaction on `RedisClient`
- Add: New `RedisZRangeResultOption` type for better interactions with zrange operations that can optionally return scores
- Add: New `RedisHashFieldKey` for type-safety when working with Hash field keys much like `RedisKey`
- Change: A few API types from enums to structs for library evolution
- Change: `RedisCommandHandler` to operate on a tuple of `RESPValue, EventLoopPromise<RESPValue>` rather than `RedisCommand`
- Change: `RedisCommand` to be a generic struct with the keyword, arguments, and a transform closure to defer execution
- Change: Almost all `RedisClient` command extensions to be factory methods on `RedisCommand` instead
- Change: Many response types to be optional to avoid developers having to do `isNull` checks on their own
- Change: `RedisClient.send(command:arguments:)` to be generic with `send(_:)` as the signature
- Rename: RedisClient extensions for scan methods to be more discoverable and legible as `scanHashField`, etc.
Result:
It should be easier to support a clean pipelining API with deferred command execution,
with extensions being easier for 2nd party developers, and the maintenance overhead of all of the command extensions
should be a little lighter when it comes to changing HOW commands are sent such as adding a context parameter
Motivation:
As originally pointed out in #48, the `map` prefix alone is not enough context into what the method actually does.
Since it fails the future, and isn't a mapping function, the name should reflect this.
But, this method as-is currently provides little value outside of the client context, so it should not be `public`.
If users provide an adequate use case for having it outside of the package, then it can be made public again.
Modifications:
- Rename: ELF where Value == RESPValue extension `map` to `tryConverting`
- Change: `tryConverting` from public to internal
Result:
A "problematic" method should no longer be available for API users to hurt themselves with.
Motivation:
Much like the change for `RedisClientError` (6471a2) any error enum that we may want to change will necessitate a major SemVer.
To avoid this, we need to use the established struct-as-enum pattern.
Modifications:
- Change: `RESPTranslator.ParsingError` to be a struct backed by an enum
Result:
The library should be able to evolve to add more `RESPTranslator.ParsingError` cases without becoming a breaking change.
Motivation:
- Newer compilers warn that "'exported: true' has no effect in
'_specialize' attribute"
- Specialize is underscored so (probably) shouldn't be relied upon
outside of the stdlib
Modifications:
- Replace @_specialize with @inlinable
Result:
Fewer warnings on more recent compilers
Motivation:
parseInteger did not distinguish between not having enough bytes for an
integer and not being able to parse the integer that was present. This
was a bit tricky for code internally, where some call sites had extra
code looking for spooky action at a distance in order to determine if
the integer failed to parse.
This is unnecessary: parseInteger is sufficiently aware of what's going
on to address this problem itself.
Modifications:
- Added a new parser error (acceptable as we haven't tagged 1.0 yet).
- Throw it from parseInteger if the integer is invalid.
Result:
parseInteger clearly communicates if the integer failed to parse.
Motivation:
ByteBufferView is not zero indexed, but parseSimpleString assumes it is.
Modifications:
- Correctly compute on the distance between two indices.
- New, somewhat contrived, test case.
Result:
No functional change: because RediStack assumes the remote peer will
always correctly terminate with /r/n, there is no point at which this
code could misbehave in the current implementation. However, with small
changes it is possible to trigger it, as the new test demonstrates.
Motivation:
When we only want the first byte, rather than create temporary
intermediate arrays we can just ask NIO to give us the first byte. This
avoids unnecessary allocations.
Modifications:
- Replace `readBytes(length: 1).first` with `readInteger(as:
UInt8.self)`
Results:
11% performance improvement in load testing due to reduced allocator
pressure on the hot path.
Motivation:
When attempting to locate a single byte, creating a transient
ByteBufferView is an excessively heavyweight operation compared to a
simple getInteger. In particular, a BBV requires retain/release
operations to enforce the CoW invariants, as well as requires jumps
through substantial amounts of generic Collection code. While this can
be specialized, so can getInteger, and getInteger has much less code in
the way to cause costs.
Modifications:
- Replace temporary view creation with getInteger.
Results:
5% performance improvement on raw throughput tests.
Motivation:
`RESPValue` exposes a fair amount of complexity on how to intialize a single instance with the various overloads.
This aims to simplify the complexity for developers by providing a single initializer and relying on `RESPValueConvertible` to handle the complexities.
In addition, the Swift complier synthesizes a lot of default conformances that `RedisKey` has manually written, which is just unnecessary code.
Modifications:
- Rename: `RESPValue.init(_:)` to `RESPValue.init(from:)`
- Change: `RESPValue.init` `String?` and `FixedWidthInteger` overloads from `public` to `internal`
- Remove: Unnecessary code for various protocol conformances for `RedisKey`
Result:
Developers should have a direct and guided way of making instances of `RESPValue`
Motivation:
Inspired by Swift by Sundell's article on type-safe identifers, the goal of this commit is to have the compiler
assist in preventing incorrect Redis key values from being used in API calls.
See https://www.swiftbysundell.com/articles/type-safe-identifiers-in-swift/ for the inspiration.
Modifications:
- Add new `RedisKey` struct that wraps around a single `String` value that conforms to several expected protocols
(Hashable, Comparable, Codable, etc.)
- Change all command APIs to require `RedisKey` rather than plain strings
Result:
When encountering an API requiring a RedisKey, it should be much more apparant at the use site what form a value should take.
Motivation:
While working to add more test coverage with `RESPTranslator`, it was made apparent that a `.bulkString(.none)` is impossible to create directly with the `RESPValue` initializers, even though it is a reasonable possibility.
Additionally, forcing all integer types to have to be stored in an `Int` is unnecessarily restrictive.
Modifications:
- Change `RESPValue.init(bulk:)` initializers to accept `Optional` instances
- Change `RESPValue.init(bulk:)` for `Int` initializer to be generic on `FixedWidthInteger`
Result:
Converting types to and from `RESPValue` should be more bi-directional and seamless.
Motivation:
Diagnostics for why `.bulkString` parses might fail were weak, and edge cases fell through gaps in coverage were found.
Modifications:
Added new cases to `RESPTranslator.ParsingError` for `.bulkString` parsing with additional test coverage.
Result:
Users should have better diagnostics for bogus data or failed parsing state.
Motivation:
There is a reasonable way to compare if two `RedisErrors` are equal, which was seen as needed in the `Equatable` conformance for `RESPValue`.
Modifications:
Added `Equatable` conformance for `RedisError` by comparing the messages.
Result:
Two `RedisError` instances are now equatable.
Motivation:
While working on unit tests the need for conformance to `Equatable` for `RESPValue` has been needed a few times and it was decided to make it public.
Modifications:
Added conformance to `Equatable` for `RESPValue` with unit test.
Result:
Users should now be able to compare two `RESPValue` instances for equality.