forked from cweatureapps/SwiftScraper
-
Notifications
You must be signed in to change notification settings - Fork 0
/
StepRunner.swift
176 lines (156 loc) · 5.46 KB
/
StepRunner.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
//
// StepRunner.swift
// SwiftScraper
//
// Created by Ken Ko on 21/04/2017.
// Copyright © 2017 Ken Ko. All rights reserved.
//
import Foundation
#if canImport(UIKit)
import UIKit
#else
import AppKit
#endif
#if canImport(UIKit)
/// Platform depended implementation of view
public typealias PlatformView = UIView
#else
/// Platform depended implementation of view
public typealias PlatformView = NSView
#endif
/// JSON dictionary.
public typealias JSON = [String: Any]
// MARK: - StepRunnerState
/// Indicates the progress and status of the `StepRunner`.
public enum StepRunnerState: Equatable {
/// Not yet started, `run()` has not been called.
case notStarted
/// The pipeline is running, and currently executing the step at the index.
case inProgress(index: Int)
/// The execution finished successfully.
case success
/// The execution failed with the given error.
case failure(error: Error)
}
/// Checks equality of the runner state, including index of currently running step
public func == (lhs: StepRunnerState, rhs: StepRunnerState) -> Bool {
switch (lhs, rhs) {
case (.notStarted, .notStarted):
return true
case (.success, .success):
return true
case (.failure, .failure):
return true
case let (.inProgress(lhsIndex), .inProgress(rhsIndex)):
return lhsIndex == rhsIndex
default:
return false
}
}
/// Checks inequality of the runner state, including index of currently running step
public func != (lhs: StepRunnerState, rhs: StepRunnerState) -> Bool {
!(lhs == rhs)
}
// MARK: - StepRunner
/// The `StepRunner` is the engine that runs the steps in the pipeline.
///
/// Once initialized, call the `run()` method to execute the steps,
/// and observe the `state` property to be notified of progress and status.
public class StepRunner {
/// The observable state which indicates the progress and status.
public private(set) var state: StepRunnerState = .notStarted {
didSet {
if state != oldValue {
for observer in stateObservers {
observer(state)
}
}
switch state {
case .success, .failure:
if let completionHandler = completion {
completionHandler()
completion = nil
}
default:
break
}
}
}
/// Callbacks which are called on each state change
public var stateObservers: [(StepRunnerState) -> Void] = []
/// A model dictionary which can be used to pass data from step to step.
public private(set) var model: JSON = [:]
private let browser: Browser
private var steps: [Step]
private var index = 0
private var completion: (() -> Void)?
/// Initializer to create the `StepRunner`.
///
/// - parameter moduleName: The name of the JavaScript module which has your customer functions.
/// By convention, the filename of the JavaScript file is the same as the module name.
/// - parameter steps: The steps to run in the pipeline.
/// - parameter scriptBundle: The bundle from which to load the JavaScript file. Defaults to the main bundle.
/// - parameter customUserAgent: The custom user agent string (only works for iOS 9+).
public init(
moduleName: String,
steps: [Step],
scriptBundle: Bundle = Bundle.main,
customUserAgent: String? = nil,
completion: (() -> Void)? = nil
) throws {
browser = try Browser(moduleName: moduleName, scriptBundle: scriptBundle, customUserAgent: customUserAgent)
self.steps = steps
self.completion = completion
}
/// Execute the steps.
public func run(completion: (() -> Void)? = nil) {
if let completion { self.completion = completion }
guard index < steps.count else {
state = .failure(error: SwiftScraperError.incorrectStep)
return
}
let stepToExecute = steps[index]
state = .inProgress(index: index)
stepToExecute.run(with: browser, model: model) { [weak self] result in
guard let self else {
return
}
model = result.model
switch result {
case .finish:
state = .success
case .proceed:
index += 1
guard index < steps.count else {
state = .success
return
}
run()
case .jumpToStep(let nextStep, _):
index = nextStep
run()
case .failure(let error, _):
state = .failure(error: error)
}
}
}
/// Resets the existing StepRunner and execute the given steps in the existing browser.
///
/// Use this to perform more steps on a StepRunner which has previously finished processing.
public func run(steps: [Step], completion: (() -> Void)? = nil) {
if let completion {
self.completion = completion
}
state = .notStarted
self.steps = steps
index = 0
run()
}
/// Insert the WebView used for scraping at index 0 of the given parent view, using AutoLayout to pin all 4 sides
/// of the parent.
///
/// Useful if the app would like to see the scraping in the foreground.
public func insertWebViewIntoView(parent: PlatformView) {
browser.insertIntoView(parent: parent)
}
}