Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
eimantas committed Aug 14, 2019
1 parent 9765577 commit 9664030
Show file tree
Hide file tree
Showing 5 changed files with 295 additions and 0 deletions.
83 changes: 83 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@

# Created by https://www.gitignore.io/api/xcode,macos,carthage,cocoapods
# Edit at https://www.gitignore.io/?templates=xcode,macos,carthage,cocoapods

### Carthage ###
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Checkouts
Carthage/Build

### CocoaPods ###
## CocoaPods GitIgnore Template

# CocoaPods - Only use to conserve bandwidth / Save time on Pushing
# - Also handy if you have a large number of dependant pods
# - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE
Pods/

### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon

# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

### Xcode ###
# Xcode
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## User settings
xcuserdata/

## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout

## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3

## Xcode Patch
*.xcodeproj/*
!*.xcodeproj/project.pbxproj
!*.xcodeproj/xcshareddata/
!*.xcworkspace/contents.xcworkspacedata
/*.gcno

### Xcode Patch ###
**/xcshareddata/WorkspaceSettings.xcsettings

# End of https://www.gitignore.io/api/xcode,macos,carthage,cocoapods
1 change: 1 addition & 0 deletions Cartfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
github "Quick/Nimble" ~> 8.0
1 change: 1 addition & 0 deletions Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
github "Quick/Nimble" "v8.0.2"
52 changes: 52 additions & 0 deletions NimbleFeedback.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,24 @@

/* Begin PBXBuildFile section */
53121D4323042AE100148925 /* NimbleFeedback.h in Headers */ = {isa = PBXBuildFile; fileRef = 53121D4123042AE100148925 /* NimbleFeedback.h */; settings = {ATTRIBUTES = (Public, ); }; };
53121D4A23042B2100148925 /* NimbleFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53121D4923042B2100148925 /* NimbleFeedback.swift */; };
53121E3123042C8800148925 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53121E1A23042C6400148925 /* Nimble.framework */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
53121D3E23042AE100148925 /* NimbleFeedback.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = NimbleFeedback.framework; sourceTree = BUILT_PRODUCTS_DIR; };
53121D4123042AE100148925 /* NimbleFeedback.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NimbleFeedback.h; sourceTree = "<group>"; };
53121D4223042AE100148925 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
53121D4923042B2100148925 /* NimbleFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NimbleFeedback.swift; sourceTree = "<group>"; };
53121E1A23042C6400148925 /* Nimble.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Nimble.framework; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
53121D3B23042AE100148925 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
53121E3123042C8800148925 /* Nimble.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -30,8 +35,10 @@
53121D3423042AE100148925 = {
isa = PBXGroup;
children = (
53121D4B23042C6400148925 /* Carthage */,
53121D4023042AE100148925 /* NimbleFeedback */,
53121D3F23042AE100148925 /* Products */,
53121E3023042C8800148925 /* Frameworks */,
);
sourceTree = "<group>";
};
Expand All @@ -48,10 +55,42 @@
children = (
53121D4123042AE100148925 /* NimbleFeedback.h */,
53121D4223042AE100148925 /* Info.plist */,
53121D4923042B2100148925 /* NimbleFeedback.swift */,
);
path = NimbleFeedback;
sourceTree = "<group>";
};
53121D4B23042C6400148925 /* Carthage */ = {
isa = PBXGroup;
children = (
53121E1823042C6400148925 /* Build */,
);
path = Carthage;
sourceTree = "<group>";
};
53121E1823042C6400148925 /* Build */ = {
isa = PBXGroup;
children = (
53121E1923042C6400148925 /* iOS */,
);
path = Build;
sourceTree = "<group>";
};
53121E1923042C6400148925 /* iOS */ = {
isa = PBXGroup;
children = (
53121E1A23042C6400148925 /* Nimble.framework */,
);
path = iOS;
sourceTree = "<group>";
};
53121E3023042C8800148925 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXHeadersBuildPhase section */
Expand Down Expand Up @@ -95,6 +134,7 @@
TargetAttributes = {
53121D3D23042AE100148925 = {
CreatedOnToolsVersion = 10.3;
LastSwiftMigration = 1030;
};
};
};
Expand Down Expand Up @@ -130,6 +170,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
53121D4A23042B2100148925 /* NimbleFeedback.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -261,13 +302,18 @@
53121D4723042AE100148925 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 2NLF64332K;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Carthage/Build/iOS",
);
INFOPLIST_FILE = NimbleFeedback/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
Expand All @@ -278,6 +324,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.trafi.NimbleFeedback;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
Expand All @@ -286,13 +333,18 @@
53121D4823042AE100148925 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 2NLF64332K;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Carthage/Build/iOS",
);
INFOPLIST_FILE = NimbleFeedback/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
Expand Down
158 changes: 158 additions & 0 deletions NimbleFeedback/NimbleFeedback.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import Foundation
import Nimble


/// A basic protocol that every state type must conform to in order to be testable.
protocol State {

associatedtype Event

/// A basic reduce that accepts current state and event and produces a new state.
///
/// - Parameters:
/// - state: current state;
/// - event: event that affects the state;
/// - Returns: New state.
static func reduce(state: Self, event: Event) -> Self
}


extension State {

/// Convenience method to mutate a state.
///
/// - Parameters:
/// - event: event to apply to receiver;
mutating func apply(_ event: Event) {
self = Self.reduce(state: self, event: event)
}
}

extension Expectation where T: State {

typealias Query<Q> = (T) -> Q?

func after(_ events: T.Event...) -> StateChangeExpectation<T> {
return StateChangeExpectation(expectation: self, events: events, change: nil)
}

func after(_ events: [T.Event]) -> StateChangeExpectation<T> {
return StateChangeExpectation(expectation: self, events: events, change: nil)
}

func after(_ change: @escaping () -> (), _ events: T.Event...) -> StateChangeExpectation<T> {
return StateChangeExpectation(expectation: self, events: events, change: change)
}

func to<Q>(_ query: @escaping Query<Q>) {
StateChangeExpectation(expectation: self, events: [], change: nil)
.to(query)
}

func toHave<Q>(_ query: @escaping Query<Q>) {
to(query)
}

func toMatch(_ matcher: @escaping (T) -> Bool) {
StateChangeExpectation(expectation: self, events: [], change: nil)
.resolve(matcher)
}
}

// MARK: - StateChangeExpectation

struct StateChangeExpectation<S: State> {

let expectation: Expectation<S>
let events: [S.Event]
let change: (() -> ())?

var noChanges: Bool {
return events.isEmpty && change == nil
}

private func afterChanges(to state: S) -> S {
change?()
return events.reduce(into: state) { $0.apply($1) }
}
}

// MARK: Query

extension StateChangeExpectation {

typealias Query<Q> = (S) -> Q?

func to<Q>(_ query: @escaping Query<Q>) {
resolve(query, toBecomeNonNil: true)
}

func toHave<Q>(_ query: @escaping Query<Q>) {
resolve(query, toBecomeNonNil: true)
}

func notTo<Q>(_ query: @escaping Query<Q>) {
resolve(query, toBecomeNonNil: false)
}

private func resolve<Q>(_ query: @escaping Query<Q>, toBecomeNonNil: Bool) {
resolve({ query($0) == nil }, to: "be a nil query", expectToFail: toBecomeNonNil)
}
}

// MARK: Match

extension StateChangeExpectation {

typealias Match = (S) -> Bool


/// An expectation that checks whether a value changes after received events.
///
/// - Parameters:
/// - matcher: a predicate to match a value against.
func toTurn(_ matcher: @escaping Match) {
resolve(matcher)
}

/// An expectation that checks whether a value remains the same after received events.
///
/// - Parameters:
/// - matcher: a predicate to match a value against.
func toStay(_ matcher: @escaping Match) {
resolve(matcher, expectSameResult: true)
}
}

// MARK: -

private extension StateChangeExpectation {

func resolve(_ matcher: @escaping Match, to msg: String = "match",
expectToFail: Bool = false,
expectSameResult: Bool = false) {

let afterTo = expectToFail ? expectation.notTo : expectation.to
let beforeTo = expectSameResult ? afterTo : expectToFail ? expectation.to : expectation.notTo

if noChanges {
afterTo(predicate("\(msg) without any events", with: matcher), nil)
} else {
beforeTo(predicate("\(msg) before applying events", with: matcher), nil)
afterTo(predicate("\(msg) after applying events", with: { matcher(self.afterChanges(to: $0)) }), nil)
}
}

func predicate(_ msg: String, with matcher: @escaping Match) -> Predicate<S> {
return Predicate { s -> PredicateResult in
let result = try s.evaluate().flatMap(matcher) ?? false
return PredicateResult(bool: result, message: .expectedTo(msg))
}
}

func apply(_ events: [S.Event], then matcher: @escaping Match) -> Match {
return { matcher(events.reduce(into: $0) { $0.apply($1) }) }
}
}


0 comments on commit 9664030

Please sign in to comment.