Skip to content

Commit

Permalink
Merge pull request #16 from BrettRToomey/docs-tests-readme
Browse files Browse the repository at this point in the history
Cleanup and doc updates for minor release.
  • Loading branch information
BrettRToomey committed Nov 26, 2016
2 parents f4cef74 + 501e4f4 commit 86a520a
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 55 deletions.
73 changes: 51 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,72 @@
# Jobs
[![Language](https://img.shields.io/badge/Swift-3-brightgreen.svg)](http://swift.org) ![Build Status](https://travis-ci.org/BrettRToomey/Jobs.svg?branch=master)[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/BrettRToomey/Jobs/master/LICENSE.md)

A job system in Swift, for Swift
A minimalistic job system in Swift, for Swift

## Getting started
Start Jobs by calling
##### Table of Contents
* [Getting started](#getting-started)
* [Intervals](#intervals)
* [Syntax candy](#syntax-candy)
* [Starting a job](#starting-a-job)
* [Stopping a job](#stopping-a-job)
* [Error handling](#error-handling)
* [Retry on failure](#retry-on-failure)

## Getting started 🚀
Creating a new `Job` is as simple as:
```swift
try Jobs.shared.start()
Jobs.add(interval: .seconds(4)) {
print("👋 I'm printed 4 times!")
}
```

And add a new `job` like so
## Intervals ⏲
The `Duration` enumeration currently supports `.seconds`, `.days` and `.weeks`.
```swift
Jobs.shared.add(interval: .seconds(4)) {
print("I am ran every 4 seconds.")
Jobs.add(interval: .days(5)) {
print("See you every 5 days.")
}
```
#### Syntax candy 🍭
It's possible to create a `Duration` from an `Int` and a `Double`.
```swift
10.seconds // `Duration.seconds(10)`
2.days // `Duration.days(2)`
3.weeks // `Duration.weeks(3)`
```

## Intervals
The `Duration` enumeration currently supports `.seconds`, `.days` and `.weeks`
## Starting a job 🎬
By default, `Job`s are started automatically, but if you wish to start one yourself, even at a later point in time, just do the following:
```swift
Jobs.shared.add(interval: .days(5)) {
print("I am ran every 5 days.")
let job = Jobs.add(interval: 2.seconds, autoStart: false) {
print("I wasn't started right away.")
}
//...
job.start()
```

## Removal
You are returned a **discardable** `JobId (UInt)` when you add a `job`. You need this `id` if you want to remove it at a later time.
## Stopping a job ✋
Giving up has never been so easy!
```swift
//keep a reference to `id` so we can use it to
//remove the job later
let id = Jobs.shared.add( //... {
// ...
}
job.stop()
```

Jobs.shared.remove(id)
## Error handling ❌
Sometimes jobs can fail, that's okay, we have you covered.
```swift
Jobs.add(
interval: 10.seconds,
action: {
throw Error.someError
},
onError: { error in
print("caught an error: \(error)")
}
)
```

## Cleanup
Stop Jobs by calling
#### Retry on failure ⭕️
By default, jobs are ran again on error. If you wish to change this behavior then set the following attribute:
```swift
try Jobs.shared.stop()
myJobName.retryOnFail = false
```
6 changes: 5 additions & 1 deletion Sources/Duration+Extensions.swift
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
extension Int {
/// Converts the integer into an enum representation of seconds.
var seconds: Duration {
return .seconds(Double(self))
}

/// Converts the integer into an enum representation of days.
var days: Duration {
return .days(self)
}

/// Converts the integer into an enum representation of weeks.
var weeks: Duration {
return .weeks(self)
}
}

extension Double {
/// Converts the real into an enum representation of seconds.
var seconds: Duration {
return .seconds(self)
}
}
}
1 change: 1 addition & 0 deletions Sources/Helpers.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import JSON
import Foundation

//TODO(Brett): change implementation to C since foundation is broken on Linux
func parseJSONFile(path: String) throws -> JSON {
let fileString = try String(contentsOfFile: path)
let json = try JSON.Parser.parse(
Expand Down
5 changes: 3 additions & 2 deletions Sources/JSON.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import JSON

extension JSON {
func buildJob() throws -> Job? {
guard case JSON.object(let dict) = self else {
guard case JSON.object = self else {
throw Error.badField("expected root object got: \(self)")
}

//TODO(Brett): implementation
//TODO(Brett): waiting on new date interface before
//continuing implementation.
return nil
}
}
10 changes: 0 additions & 10 deletions Sources/JobHandler.swift

This file was deleted.

66 changes: 55 additions & 11 deletions Sources/Jobs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import Core
import Dispatch
import Foundation

/// Represents an amount of time.
public enum Duration {
case seconds(Double)
case days(Int)
case weeks(Int)
}

extension Duration {
/// Converts the enumeration representation of time into a `Double`.
public var unixTime: Double {
switch self {
case .seconds(let count):
Expand All @@ -31,21 +33,26 @@ protocol Performable {
func perform()
}

/// A scheduled, performable task.
public class Job: Performable {
public typealias Action = (Void) throws -> Void
public typealias ErrorCallback = (Error) -> Void

/// The job's name.
public var name: String?

/// The current state of the job.
public var isRunning: Bool

//TODO(Brett): currently not being used, will add with job batches.
var lastPerformed: Double

/// Whether or not the job will retry on failure.
public var retryOnFail = true

var interval: Double
let action: Action
let errorCallback: ErrorCallback?

let lock = Lock()

init(
name: String?,
interval: Double,
Expand All @@ -57,10 +64,10 @@ public class Job: Performable {
self.action = action
self.errorCallback = errorCallback

lastPerformed = 0
isRunning = false
}

/// Starts a pending job.
public func start() {
guard !isRunning else {
return
Expand All @@ -70,6 +77,7 @@ public class Job: Performable {
perform()
}

/// Stops a job.
public func stop() {
lock.locked {
isRunning = false
Expand All @@ -79,28 +87,48 @@ public class Job: Performable {
func perform() {
lock.locked {
if isRunning {
let failedToRun: Bool
do {
try action()
failedToRun = false
} catch {
if let errorCallback = errorCallback {
errorCallback(error)
}
failedToRun = true
}
if failedToRun && retryOnFail {
//make sure we leave the lock first.
defer {
Jobs.shared.queue(self, performNow: true)
}
} else {
Jobs.shared.queue(self)
}

Jobs.shared.queue(self)
}
}
}
}

public final class Jobs {
//consider making `lock` and `workerQueue` static to remove singleton.
static let shared = Jobs()

var isRunning: Bool = false

let lock = Lock()
let workerQueue = DispatchQueue(label: "jobs-worker")

/**
Registers a new `Job` with the provided properties.
- Parameters:
- name: The name of the job.
- interval: How often the job is performed.
- autoStart: Whether or not to start the job automatically.
- action: The action to perform.
- onError: An `Optional` error handler closure.
- Returns: The instantiated `Job`.
*/
@discardableResult
public static func add(
name: String? = nil,
Expand All @@ -124,15 +152,31 @@ public final class Jobs {
return job
}

//enables shorthand for the closure when an error callback isn't required.
/**
Registers a new `Job` with the provided properties.
- Parameters:
- name: The name of the job.
- interval: How often the job is performed.
- autoStart: Whether or not to start the job automatically.
- action: The action to perform.
- Returns: The instantiated `Job`.
*/
@discardableResult
public static func add(
name: String? = nil,
interval: Duration,
autoStart: Bool = true,
action: @escaping Job.Action
) -> Job {
return add(name: name, interval: interval, autoStart: autoStart, action: action, onError: nil)
return add(
name: name,
interval: interval,
autoStart: autoStart,
action: action,
onError: nil
)
}

func queue(_ job: Performable, performNow: Bool = false) {
Expand Down
24 changes: 22 additions & 2 deletions Sources/Shell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,19 @@ import Foundation
#endif

enum ShellError: Error {
/// Thrown when the command didn't error but data failed to unwrap.
case failedToUnwrapOutput
}

/**
Launch any process and log its output.
- Parameters:
- path: The path of the process to launch.
- args: The arguments to pass to the launched process.
- Returns: `STDOUT`
*/
@discardableResult
public func shell(path launchPath: String, args arguments: [String]) throws -> String {
let process = Process()
Expand All @@ -35,11 +45,21 @@ public func shell(path launchPath: String, args arguments: [String]) throws -> S
return result
}

/**
Execute any `bash` command and log its output.
- Parameters:
- command: The command to be executed.
- args: The argumentes to pass to the interpreter.
- Returns: `STDOUT`
*/
@discardableResult
public func bash(command: String, arguments: [String]) throws -> String {
public func bash(command: String, args: [String]) throws -> String {
let whichPathForCommand = try shell(
path: "/bin/bash",
args: [ "-l", "-c", "which \(command)" ]
)
return try shell(path: whichPathForCommand, args: arguments)
return try shell(path: whichPathForCommand, args: args)
}
2 changes: 1 addition & 1 deletion Tests/JobsTests/ShellTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class ShellTests: XCTestCase {

func testBashCommand() {
do {
let output = try bash(command: "echo", arguments: ["hello, world!"])
let output = try bash(command: "echo", args: ["hello, world!"])
XCTAssertEqual(output, "hello, world!")
} catch {
XCTFail(error.localizedDescription)
Expand Down

0 comments on commit 86a520a

Please sign in to comment.