Mordil/RediStack

View on GitHub
Sources/RediStack/Commands/SetCommands.swift

Summary

Maintainability
A
50 mins
Test Coverage
//===----------------------------------------------------------------------===//
//
// This source file is part of the RediStack open source project
//
// Copyright (c) 2019-2022 RediStack project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of RediStack project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import struct Logging.Logger
import NIOCore

// MARK: Sets

extension RedisCommand {
    /// [SADD](https://redis.io/commands/sadd)
    /// - Parameters:
    ///     - elements: The values to add to the set.
    ///     - key: The key of the set to insert into.
    @inlinable
    public static func sadd<Value: RESPValueConvertible>(_ elements: [Value], to key: RedisKey) -> RedisCommand<Int> {
        assert(elements.count > 0, "at least 1 element should be provided")

        var args = [RESPValue(from: key)]
        args.append(convertingContentsOf: elements)
        
        return .init(keyword: "SADD", arguments: args)
    }

    /// [SADD](https://redis.io/commands/sadd)
    /// - Parameters:
    ///     - elements: The values to add to the set.
    ///     - key: The key of the set to insert into.
    @inlinable
    public static func sadd<Value: RESPValueConvertible>(_ elements: Value..., to key: RedisKey) -> RedisCommand<Int> {
        return .sadd(elements, to: key)
    }

    /// [SCARD](https://redis.io/commands/scard)
    /// - Parameter key: The key of the set.
    public static func scard(of key: RedisKey) -> RedisCommand<Int> {
        let args = [RESPValue(from: key)]
        return .init(keyword: "SCARD", arguments: args)
    }

    /// [SDIFF](https://redis.io/commands/sdiff)
    /// - Parameter keys: The source sets to calculate the difference of.
    public static func sdiff(of keys: [RedisKey]) -> RedisCommand<[RESPValue]> {
        assert(!keys.isEmpty, "at least 1 key should be provided")

        let args = keys.map(RESPValue.init(from:))
        return .init(keyword: "SDIFF", arguments: args)
    }

    /// [SDIFF](https://redis.io/commands/sdiff)
    /// - Parameter keys: The source sets to calculate the difference of.
    public static func sdiff(of keys: RedisKey...) -> RedisCommand<[RESPValue]> { .sdiff(of: keys) }

    /// [SDIFFSTORE](https://redis.io/commands/sdiffstore)
    /// - Warning: If the `destination` key already exists, its value will be overwritten.
    /// - Parameters:
    ///     - destination: The key of the new set from the result.
    ///     - sources: The list of source sets to calculate the difference of.
    public static func sdiffstore(as destination: RedisKey, sources keys: [RedisKey]) -> RedisCommand<Int> {
        assert(!keys.isEmpty, "at least 1 key should be provided")

        var args = [RESPValue(from: destination)]
        args.append(convertingContentsOf: keys)

        return .init(keyword: "SDIFFSTORE", arguments: args)
    }

    /// [SINTER](https://redis.io/commands/sinter)
    /// - Parameter keys: The source sets to calculate the intersection of.
    public static func sinter(of keys: [RedisKey]) -> RedisCommand<[RESPValue]> {
        assert(!keys.isEmpty, "at least 1 key should be provided")

        let args = keys.map(RESPValue.init(from:))
        return .init(keyword: "SINTER", arguments: args)
    }

    /// [SINTER](https://redis.io/commands/sinter)
    /// - Parameter keys: The source sets to calculate the intersection of.
    public static func sinter(of keys: RedisKey...) -> RedisCommand<[RESPValue]> { .sinter(of: keys) }

    /// [SINTERSTORE](https://redis.io/commands/sinterstore)
    /// - Warning: If the `destination` key already exists, its value will be overwritten.
    /// - Parameters:
    ///     - destination: The key of the new set from the result.
    ///     - sources: A list of source sets to calculate the intersection of.
    public static func sinterstore(as destination: RedisKey, sources keys: [RedisKey]) -> RedisCommand<Int> {
        assert(!keys.isEmpty, "at least 1 key should be provided")

        var args = [RESPValue(from: destination)]
        args.append(convertingContentsOf: keys)
        
        return .init(keyword: "SINTERSTORE", arguments: args)
    }

    /// [SISMEMBER](https://redis.io/commands/sismember)
    /// - Parameters:
    ///     - element: The element to look for in the set.
    ///     - key: The key of the set to look in.
    @inlinable
    public static func sismember<Value: RESPValueConvertible>(_ element: Value, of key: RedisKey) -> RedisCommand<Bool> {
        let args: [RESPValue] = [
            .init(from: key),
            element.convertedToRESPValue()
        ]
        return .init(keyword: "SISMEMBER", arguments: args)
    }

    /// [SMEMBERS](https://redis.io/commands/smembers)
    /// - Note: Ordering of results are stable between multiple calls of this method to the same set.
    ///
    /// Results are **UNSTABLE** in regards to the ordering of insertions through the `sadd` command and this method.
    /// - Parameter key: The key of the set.
    public static func smembers(of key: RedisKey) -> RedisCommand<[RESPValue]> {
        let args = [RESPValue(from: key)]
        return .init(keyword: "SMEMBERS", arguments: args)
    }

    /// [SMOVE](https://redis.io/commands/smove)
    /// - Parameters:
    ///     - element: The value to move from the source.
    ///     - sourceKey: The key of the source set.
    ///     - destKey: The key of the destination set.
    @inlinable
    public static func smove<Value: RESPValueConvertible>(
        _ element: Value,
        from sourceKey: RedisKey,
        to destKey: RedisKey
    ) -> RedisCommand<Bool> {
        assert(sourceKey != destKey, "same key was provided for a move operation")

        let args: [RESPValue] = [
            .init(from: sourceKey),
            .init(from: destKey),
            element.convertedToRESPValue()
        ]
        return .init(keyword: "SMOVE", arguments: args)
    }

    /// [SPOP](https://redis.io/commands/spop)
    /// - Parameters:
    ///     - key: The key of the set.
    ///     - count: The max number of elements to pop from the set.
    public static func spop(from key: RedisKey, max count: Int = 1) -> RedisCommand<[RESPValue]> {
        assert(count >= 0, "a negative max count is nonsense")

        let args: [RESPValue] = [
            .init(from: key),
            .init(bulk: count)
        ]
        return .init(keyword: "SPOP", arguments: args)
    }

    /// [SRANDMEMBER](https://redis.io/commands/srandmember)
    ///
    /// Example usage:
    /// ```swift
    /// // pull just 1 random element
    /// client.send(.srandmember(from: "my_key"))
    ///
    /// // pulls up to 3 elements, allowing duplicates
    /// client.send(.srandmember(from: "my_key", max: -3))
    ///
    /// // pulls up to 3 unique elements
    /// client.send(.srandmember(from: "my_key", max: 3))
    /// ```
    /// - Parameters:
    ///     - key: The key of the set.
    ///     - count: The max number of elements to select from the set.
    public static func srandmember(from key: RedisKey, max count: Int = 1) -> RedisCommand<[RESPValue]> {
        let args: [RESPValue] = [
            .init(from: key),
            .init(bulk: count)
        ]
        return .init(keyword: "SRANDMEMBER", arguments: args)
    }

    /// [SREM](https://redis.io/commands/srem)
    /// - Parameters:
    ///     - elements: The values to remove from the set.
    ///     - key: The key of the set to remove from.
    @inlinable
    public static func srem<Value: RESPValueConvertible>(_ elements: [Value], from key: RedisKey) -> RedisCommand<Int> {
        var args: [RESPValue] = [.init(from: key)]
        args.append(convertingContentsOf: elements)
        return .init(keyword: "SREM", arguments: args)
    }

    /// [SREM](https://redis.io/commands/srem)
    /// - Parameters:
    ///     - elements: The values to remove from the set.
    ///     - key: The key of the set to remove from.
    @inlinable
    public static func srem<Value: RESPValueConvertible>(_ elements: Value..., from key: RedisKey) -> RedisCommand<Int> {
        return .srem(elements, from: key)
    }

    /// [SUNION](https://redis.io/commands/sunion)
    /// - Parameter keys: The source sets to calculate the union of.
    public static func sunion(of keys: [RedisKey]) -> RedisCommand<[RESPValue]> {
        let args = keys.map(RESPValue.init(from:))
        return .init(keyword: "SUNION", arguments: args)
    }

    /// [SUNION](https://redis.io/commands/sunion)
    /// - Parameter keys: The source sets to calculate the union of.
    public static func sunion(of keys: RedisKey...) -> RedisCommand<[RESPValue]> { .sunion(of: keys) }

    /// [SUNIONSTORE](https://redis.io/commands/sunionstore)
    /// - Parameters:
    ///     - destination: The key of the new set from the result.
    ///     - sources: A list of source sets to calculate the union of.
    public static func sunionstore(as destination: RedisKey, sources keys: [RedisKey]) -> RedisCommand<Int> {
        assert(!keys.isEmpty, "at least 1 key should be provided")

        var args = [RESPValue(from: destination)]
        args.append(convertingContentsOf: keys)

        return .init(keyword: "SUNIONSTORE", arguments: args)
    }

    /// [SSCAN](https://redis.io/commands/sscan)
    /// - Parameters:
    ///     - key: The key of the set.
    ///     - position: The position to start the scan from.
    ///     - count: The number of elements to advance by. Redis default is 10.
    ///     - match: A glob-style pattern to filter values to be selected from the result set.
    public static func sscan(
        _ key: RedisKey,
        startingFrom position: Int = 0,
        matching match: String? = nil,
        count: Int? = nil
    ) -> RedisCommand<(Int, [RESPValue])> {
        return ._scan(keyword: "SSCAN", key, position, match, count, { try $0.map() })
    }
}

// MARK: -

extension RedisClient {
    /// Incrementally iterates over all values in a set.
    ///
    /// See ``RedisCommand/sscan(_:startingFrom:matching:count:)``
    /// - Parameters:
    ///     - key: The key of the set.
    ///     - position: The position to start the scan from.
    ///     - count: The number of elements to advance by. Redis default is 10.
    ///     - match: A glob-style pattern to filter values to be selected from the result set.
    ///     - eventLoop: An optional event loop to hop to for any further chaining on the returned event loop future.
    ///     - logger: An optional logger instance to use for logs generated from this command.
    /// - Returns: A `NIO.EventLoopFuture` that resolves a cursor position for additional scans, with a limited collection of values that were iterated over.
    public func scanSetValues(
        in key: RedisKey,
        startingFrom position: Int = 0,
        matching match: String? = nil,
        count: Int? = nil,
        eventLoop: EventLoop? = nil,
        logger: Logger? = nil
    ) -> EventLoopFuture<(Int, [RESPValue])> {
        return self.send(.sscan(key, startingFrom: position, matching: match, count: count), eventLoop: eventLoop, logger: logger)
    }
}