Skip to content

Instantly share code, notes, and snippets.

@a-voronov
Last active January 29, 2019 17:07
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save a-voronov/12fcc2139fa2d14e31b256b57ef83f27 to your computer and use it in GitHub Desktop.
Save a-voronov/12fcc2139fa2d14e31b256b57ef83f27 to your computer and use it in GitHub Desktop.
Lightweight Random utils inspired by RandomKit, using seedable Xoroshiro generator from CwlUtils. Sourcery templates included.
import Foundation
{# So far getting imports hardcoded from config, correct approach might be found here: https://github.com/krzysztofzablocki/Sourcery/issues/670 #}
{% for import in argument.imports %}
import {{ import }}
{% endfor %}
{% if argument.testable %}{% for testable in argument.testable %}
@testable import {{ testable }}
{% endfor %}{% endif %}
{% macro randomValue type %}{% if type.kind == "protocol" %}{{ type.inheritedTypes.0.name }}{% endif %}{% endmacro %}
// MARK: - Structs
{# Random Struct #}
{% macro rng %}&{{ argument.rng }}{% endmacro %}
{% macro customRandomValueType variable %}{% if variable.annotations.random %}{{ variable.annotations.random }}{% endif %}{% endmacro %}
{% macro randomValueUsingGenerator variable %}{% call customRandomValueType variable %}{% if variable.isTuple %}randomTuple(using: &generator){% else %}.random(using: &generator){% endif %}{% endmacro %}
{% macro randomValueUsingRNG variable %}{% call customRandomValueType variable %}{% if variable.isTuple %}randomTuple(using: {% call rng %}){% else %}.random(using: {% call rng %}){% endif %}{% endmacro %}
{% for type in types.structs where type|annotated:"Random" %}
extension {{ type.name }}: Random {
public static func random<G: RandomNumberGenerator>(using generator: inout G) -> {{ type.name }} {
return {{ type.name }}(
{% for variable in type.variables where not variable.isComputed %}
{{ variable.name }}: {% call randomValueUsingGenerator variable %}{% if not forloop.last %},{% endif %}
{% endfor %}
)
}
{% if argument.rng %}
{# user-friendly version, so that you don't have to bother with closures and incoming generators. But it's tightly coupled to `rng` argument value from sourcery config #}
public static func random(
{% for variable in type.variables where not variable.isComputed %}
{{ variable.name }}: {{ variable.typeName }} = {% call randomValueUsingRNG variable %}{% if not forloop.last %},{% endif %}
{% endfor %}
) -> {{ type.name }} {
return {{ type.name }}(
{% for variable in type.variables where not variable.isComputed %}
{{ variable.name }}: {{ variable.name }}{% if not forloop.last %},{% endif %}
{% endfor %}
)
}
{% else %}
{# this one is generated additionally, so that you can have everything random except for some selected fields #}
public static func random<G: RandomNumberGenerator>(
_ generator: inout G,
{% for variable in type.variables where not variable.isComputed %}
{{ variable.name }}: (inout G) -> {{ variable.typeName }} = { generator in {% call randomValueUsingGenerator variable %} }{% if not forloop.last %},{% endif %}
{% endfor %}
) -> {{ type.name }} {
return {{ type.name }}(
{% for variable in type.variables where not variable.isComputed %}
{{ variable.name }}: {{ variable.name }}(&generator){% if not forloop.last %},{% endif %}
{% endfor %}
)
}
{% endif %}
}
{% endfor %}
// MARK: - Enums
{# Random Enum #}
{% macro randomEnumCaseUsingGenerator case %}.{{ case.name }}{% if case.hasAssociatedValue %}({% for value in case.associatedValues %}{% if value.localName %}{{ value.localName }}: {% endif %}{% call randomValueUsingGenerator value %}{% if not forloop.last %}, {% endif %}{% endfor %}){% endif %}{% endmacro %}
{% macro randomIfCaseUsingGenerator case %}{% if case.hasAssociatedValue %}{{ case.name }}(&generator){% else %}.{{ case.name }}{% endif %}{% endmacro %}
{% macro randomEnumCaseUsingRNG case %}.{{ case.name }}{% if case.hasAssociatedValue %}({% for value in case.associatedValues %}{% if value.localName %}{{ value.localName }}: {% endif %}{% call randomValueUsingRNG value %}{% if not forloop.last %}, {% endif %}{% endfor %}){% endif %}{% endmacro %}
{% macro randomIfCaseUsingRNG case %}{% if case.hasAssociatedValue %}{{ case.name }}{% else %}.{{ case.name }}{% endif %}{% endmacro %}
{% for enum in types.enums where enum|annotated:"Random" %}
{% if enum.cases.count == 0 %}
#warning("`{{ enum.name }}` is an uninhabitant type and no value of it can be created, thus it can't have random value as well.")
// extension {{ enum.name }}: Random { }
{% elif not enum.hasAssociatedValues %}
extension {{ enum.name }}: Random {
{# I'd expect to check `enum.based` property, but it's always empty ¯\_(ツ)_/¯ #}
{% if enum.rawTypeName.name != "CaseIterable" and enum.inheritedTypes|join:" "|!contains:"CaseIterable" %}
#warning("Please, conform to `CaseIterable` protocol, so that compiler takes care of this")
public static let allCases: [{{ enum.name }}] = [{% for case in enum.cases %}.{{ case.name }}{% if not forloop.last %}, {% endif %}{% endfor %}]
{% endif %}
public static func random<G: RandomNumberGenerator>(using generator: inout G) -> {{ enum.name }} {
return allCases.randomElement(using: &generator)!
}
}
{% else %}
extension {{ enum.name }}: RandomAll {
public static func allRandom<G: RandomNumberGenerator>(using generator: inout G) -> [{{ enum.name }}] {
{% if enum.cases.count == 1 %}
return [{% call randomEnumCaseUsingGenerator enum.cases.0 %}]
{% else %}
return [{% for case in enum.cases %}
{% call randomEnumCaseUsingGenerator case %}{% if not forloop.last %},{% endif %}{% endfor %}
]
{% endif %}
}
{#
alternative version, so that you don't have to bother with closures and incoming generators
but it's tightly coupled to `rng` argument value from sourcery config
and it executes and calculates randoms for all cases even though only one will be needed (thus a bit more expensive)
#}
{% if argument.rng %}
public static func random(
{% for case in enum.cases where case.associatedValues %}
{{ case.name }}: {{ enum.name }} = {% call randomEnumCaseUsingRNG case %}{% if not forloop.last %},{% endif %}
{% endfor %}
) -> {{ enum.name }} {
{% if enum.cases.count == 1 %}
return {% call randomIfCaseUsingRNG enum.cases.0 %}
{% else %}
return [
{% for case in enum.cases %}
{% call randomIfCaseUsingRNG case %}{% if not forloop.last %},{% endif %}
{% endfor %}
].randomElement(using: {% call rng %})!
{% endif %}
}
{% else %}
{# same for enum - you can override `random` for some cases and leave others generated by default #}
public static func random<G: RandomNumberGenerator>(
_ generator: inout G,
{% for case in enum.cases where case.associatedValues %}
{{ case.name }}: (inout G) -> {{ enum.name }} = { generator in {% call randomEnumCaseUsingGenerator case %} }{% if not forloop.last %},{% endif %}
{% endfor %}
) -> {{ enum.name }} {
{% if enum.cases.count == 1 %}
return {% call randomIfCaseUsingGenerator enum.cases.0 %}
{% else %}
return [
{% for case in enum.cases %}
{% call randomIfCaseUsingGenerator case %}{% if not forloop.last %},{% endif %}
{% endfor %}
].randomElement(using: &generator)!
{% endif %}
}
{% endif %}
}
{% endif %}
{% endfor %}
// MARK: - Random
public protocol Random {
static func random<G: RandomNumberGenerator>(using generator: inout G) -> Self
}
// MARK: - Dictionary
extension Dictionary: Random where Key: Random, Value: Random {
public static func random<G: RandomNumberGenerator>(using generator: inout G) -> [Key: Value] {
return random(ofLength: 5, using: &generator)
}
}
extension Dictionary where Key: Random, Value: Random {
public static func random<G: RandomNumberGenerator>(ofLength length: UInt, using generator: inout G) -> [Key: Value] {
return (0 ..< length).reduce([:]) { (acc, _) in
var res = acc
res[Key.random(using: &generator)] = Value.random(using: &generator)
return res
}
}
}
extension Dictionary where Key: Hashable & Strideable, Value: Hashable & Strideable {
public static func random<G: RandomNumberGenerator>(ofLength length: UInt, inKeys keys: [Key], inValues values: [Value], using generator: inout G) -> [Key: Value] {
precondition(!keys.isEmpty, "keys range shouldn't be empty")
precondition(!values.isEmpty, "values range shouldn't be empty")
return (0 ..< length).reduce([:]) { (acc, _) in
var res = acc
res[keys.randomElement(using: &generator)!] = values.randomElement(using: &generator)!
return res
}
}
}
// MARK: - Set
extension Set: Random where Element: Random {
public static func random<G: RandomNumberGenerator>(using generator: inout G) -> Set<Element> {
return random(ofLength: 5, using: &generator)
}
}
extension Set where Element: Random {
public static func random<G: RandomNumberGenerator>(ofLength length: UInt, using generator: inout G) -> Set<Element> {
var buffer = Set()
while buffer.count != length {
buffer.insert(Element.random(using: &generator))
}
return buffer
}
}
// MARK: - Array
extension Array: Random where Element: Random {
public static func random<G: RandomNumberGenerator>(using generator: inout G) -> [Element] {
return random(ofLength: 5, using: &generator)
}
}
extension Array where Element: Random {
public static func random<G: RandomNumberGenerator>(ofLength length: UInt, using generator: inout G) -> [Element] {
return (0 ..< length).map { _ in Element.random(using: &generator) }
}
}
extension Array {
public static func random<G: RandomNumberGenerator>(ofLength length: UInt, from array: [Element], using generator: inout G) -> [Element] {
guard !array.isEmpty else { return [] }
return (0 ..< length).map { _ in array.randomElement(using: &generator)! }
}
}
// MARK: - String
extension String: Random {
public static func random<G: RandomNumberGenerator>(using generator: inout G) -> String {
var result = UnicodeScalarView()
for _ in 0 ..< 10 {
result.append(UnicodeScalar.random(using: &generator))
}
return String(result)
}
}
extension String {
public static func random<G: RandomNumberGenerator>(ofLength length: UInt, from string: String, using generator: inout G) -> String {
guard !string.isEmpty else { return "" }
return (0 ..< length).reduce("") { (acc, _) in
var res = acc
res.append(string.randomElement(using: &generator)!)
return res
}
}
}
// MARK: - Character
extension Character: Random {
public static func random<G: RandomNumberGenerator>(using generator: inout G) -> Character {
return Character(UnicodeScalar.random(using: &generator))
}
}
extension Character {
// Can't convert Character into UnicodeScalar or into UInt, thus constraining it with UnicodeScalar range
public static func random<G: RandomNumberGenerator>(in closedRange: ClosedRange<UnicodeScalar>, using generator: inout G) -> Character {
return Character(UnicodeScalar.random(in: closedRange, using: &generator))
}
}
extension Character {
// Can't convert Character into UnicodeScalar or into UInt, thus constraining it with UnicodeScalar range
public static func random<G: RandomNumberGenerator>(in range: Range<UnicodeScalar>, using generator: inout G) -> Character {
return Character(UnicodeScalar.random(in: range, using: &generator))
}
}
// MARK: - UnicodeScalar
extension UnicodeScalar: Random {
static let randomRange: ClosedRange<UnicodeScalar> = " " ... "~"
public static func random<G: RandomNumberGenerator>(using generator: inout G) -> UnicodeScalar {
return UnicodeScalar.random(in: randomRange, using: &generator)
}
}
extension UnicodeScalar {
public static func random<G: RandomNumberGenerator>(in closedRange: ClosedRange<UnicodeScalar>, using generator: inout G) -> UnicodeScalar {
let newRange = ClosedRange(uncheckedBounds: (lower: closedRange.lowerBound.value, upper: closedRange.upperBound.value))
let random = UInt32.random(in: newRange, using: &generator)
return UnicodeScalar(random)!
}
}
extension UnicodeScalar {
public static func random<G: RandomNumberGenerator>(in range: Range<UnicodeScalar>, using generator: inout G) -> UnicodeScalar {
let newRange = Range(uncheckedBounds: (lower: range.lowerBound.value, upper: range.upperBound.value))
let random = UInt32.random(in: newRange, using: &generator)
return UnicodeScalar(random)!
}
}
// MARK: - FloatingPoint
extension FloatingPoint where Self: Random {
public static func random<G: RandomNumberGenerator>(using generator: inout G) -> Self {
return Self(UInt.random(using: &generator)) / Self(UInt.max)
}
}
extension Float: Random { }
extension Double: Random { }
// MARK: - Integer
extension FixedWidthInteger where Self: Random {
public static func random<G: RandomNumberGenerator>(using generator: inout G) -> Self {
return Self.random(in: Self.min...Self.max, using: &generator)
}
}
// MARK: Int
extension Int: Random { }
extension Int64: Random { }
extension Int32: Random { }
extension Int16: Random { }
extension Int8: Random { }
// MARK: UInt
extension UInt: Random { }
extension UInt64: Random { }
extension UInt32: Random { }
extension UInt16: Random { }
extension UInt8: Random { }
// MARK: - Bool
extension Bool: Random { }
// MARK: - Tuples
public func randomTuple<
A: Random,
B: Random,
G: RandomNumberGenerator
>(using generator: inout G) -> (A, B) {
return (
.random(using: &generator),
.random(using: &generator)
)
}
public func randomTuple<
A: Random,
B: Random,
C: Random,
G: RandomNumberGenerator
>(using generator: inout G) -> (A, B, C) {
return (
.random(using: &generator),
.random(using: &generator),
.random(using: &generator)
)
}
public func randomTuple<
A: Random,
B: Random,
C: Random,
D: Random,
G: RandomNumberGenerator
>(using generator: inout G) -> (A, B, C, D) {
return (
.random(using: &generator),
.random(using: &generator),
.random(using: &generator),
.random(using: &generator)
)
}
// MARK: - RandomAll
public protocol RandomAll: Random {
static func allRandom<G: RandomNumberGenerator>(using generator: inout G) -> [Self]
}
extension RandomAll {
public static func random<G: RandomNumberGenerator>(using generator: inout G) -> Self {
return allRandom(using: &generator).randomElement(using: &generator)!
}
}
extension RandomAll {
public static func allRandom<G: RandomNumberGenerator>(where predicate: (Self) -> Bool, using generator: inout G) -> [Self] {
return allRandom(using: &generator).filter(predicate)
}
public static func allRandom<G: RandomNumberGenerator>(except predicate: (Self) -> Bool, using generator: inout G) -> [Self] {
return allRandom(using: &generator).filter { !predicate($0) }
}
}
extension RandomAll where Self: Equatable {
public static func allRandom<G: RandomNumberGenerator>(where item: Self, using generator: inout G) -> [Self] {
return allRandom(where: { $0 == item }, using: &generator)
}
public static func allRandom<G: RandomNumberGenerator>(except item: Self, using generator: inout G) -> [Self] {
return allRandom(except: { $0 == item }, using: &generator)
}
}
extension RandomAll {
public static func allRandom<G: RandomNumberGenerator>(where keyPath: KeyPath<Self, Bool>, using generator: inout G) -> [Self] {
return allRandom(where: { $0[keyPath: keyPath] }, using: &generator)
}
public static func allRandom<G: RandomNumberGenerator>(except keyPath: KeyPath<Self, Bool>, using generator: inout G) -> [Self] {
return allRandom(except: { $0[keyPath: keyPath] }, using: &generator)
}
}
// MARK: - Optional
extension Optional: RandomAll where Wrapped: Random {
public static func allRandom<G: RandomNumberGenerator>(using generator: inout G) -> [Wrapped?] {
return [.none, .some(.random(using: &generator))]
}
}
extension Optional: Random where Wrapped: Random {}
// Copyright (c) 2008-2018 Matt Gallagher (http://cocoawithlove.com). All rights reserved.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
// FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
// NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH
// THE USE OR PERFORMANCE OF THIS SOFTWARE.
public struct Xoroshiro: RandomNumberGenerator {
public typealias State = (UInt64, UInt64, UInt64, UInt64)
public private(set) var state: State = (0, 0, 0, 0)
public init() {
var generator = SystemRandomNumberGenerator()
state = randomTuple(using: &generator)
}
public init(seed: State) {
state = seed
}
public mutating func next() -> UInt64 {
// Derived from public domain implementation of xoshiro256** here:
// http://xoshiro.di.unimi.it
// by David Blackman and Sebastiano Vigna
let x = state.1 &* 5
let result = ((x &<< 7) | (x &>> 57)) &* 9
let t = state.1 &<< 17
state.2 ^= state.0
state.3 ^= state.1
state.1 ^= state.2
state.0 ^= state.3
state.2 ^= t
state.3 = (state.3 &<< 45) | (state.3 &>> 19)
return result
}
}
@a-voronov
Copy link
Author

a-voronov commented Oct 22, 2018

Random

Swift: 4.2

  • Random protocol is mostly needed to know which entities can be generated and thus to extend existing API using this knowledge.
  • RandomAll protocol is mostly needed for enums with associated values to implement (there's CaseIterable for ones without associated values).
  • Xoroshiro is needed as a seedable generator (very useful when repeating tests with data generated randomly) and is taken from CwlUtils.
  • random.stencil Sourcery template is used to automatically extend Structs and Enums to support Random or RandomAll protocols.
    • sourcery config supports custom arguments via args key:
      • testable - which adds @testable imports (list).
      • imports - which just adds needed imports as we're not able to tell 100% what type is used from what Module (list).
      • rng - in case you already have globally accessible RNG variable, it will generate additional random helper method for your Struct with default (random) values for each property

Example

# .sourcery.yml

sources:
  - ./Sources
templates:
  - ./Templates
output:
  path: ./Sources/Generated
args:
  testable:
    - YourApp
  imports:
    - Result
  rng: R
// Structs & Enums

// this random generator `R` is mentioned as `rng` in sourcery config 
// and it will also be used while generating struct random methods with default arguments
var R = Xoroshiro()

// sourcery: Random
struct User {
    let id: String
    let name: String
    let age: Int
    let gender: Gender
    let status: Status
    let friends: (followers: Int, followees: Int)
}

// sourcery: Random
enum Status {
    case loggedIn
    // don't know how to generate random from protocol? no worries, just mention known type
    case loggedOut(/* sourcery: random = String */Error?)
}

extension String: Error {}

// sourcery: Random
// if you don't conform to `CaseIterable`, `allCases` will be generated for you, but with #warning
enum Gender: CaseIterable {
    case male, female, other
}

As a result, you end up with 2 versions of random methods generated:

  • plain random, that doesn't accept any arguments except for generator
  • either random with arguments for each property (if no rng defined in Sourcery config), with generated randoms by default, but giving ability to customize (via closures)
  • or random with more user-friendly API (if rng defined in Sourcery config), but tight-coupling to sourcery config rng argument value (and might seem inefficiency in case of enums)
// Generated

// MARK: - Structs

extension User: Random {
    public static func random<G: RandomNumberGenerator>(using generator: inout G) -> User {
        return User(
            id: .random(using: &generator),
            name: .random(using: &generator),
            age: .random(using: &generator),
            gender: .random(using: &generator),
            status: .random(using: &generator),
            friends: randomTuple(using: &generator)
        )
    }

    public static func random(
        id: String = .random(using: &R),
        name: String = .random(using: &R),
        age: Int = .random(using: &R),
        gender: Gender = .random(using: &R),
        status: Status = .random(using: &R),
        friends: (followers: Int, followees: Int) = randomTuple(using: &R)
    ) -> User {
        return User(
            id: id,
            name: name,
            age: age,
            gender: gender,
            status: status,
            friends: friends
        )
    }
}

// MARK: - Enums

extension Gender: Random {

    public static func random<G: RandomNumberGenerator>(using generator: inout G) -> Gender {
        return allCases.randomElement(using: &generator)!
    }
}

extension Status: RandomAll {
    public static func allRandom<G: RandomNumberGenerator>(using generator: inout G) -> [Status] {
        return [
            .loggedIn,
            .loggedOut(String.random(using: &generator))
        ]
    }

    public static func random(
        loggedOut: Status = .loggedOut(String.random(using: &R))
    ) -> Status {
        return [
            .loggedIn,
            loggedOut
        ].randomElement(using: &R)!
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment