diff --git a/Commandant.xcodeproj/project.pbxproj b/Commandant.xcodeproj/project.pbxproj index cdddc7f..ffd094b 100644 --- a/Commandant.xcodeproj/project.pbxproj +++ b/Commandant.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 680759651FC5A25D00D6CA70 /* OptionsWithEnumProtocolSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680759631FC5A1F600D6CA70 /* OptionsWithEnumProtocolSpec.swift */; }; CD2ED3411C1E6C5D0076092B /* Argument.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2ED3401C1E6C5D0076092B /* Argument.swift */; }; CD2ED3431C1E6D540076092B /* ArgumentProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2ED3421C1E6D540076092B /* ArgumentProtocol.swift */; }; CDCE78341FBAB047005A9F76 /* OrderedSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCE78331FBAB047005A9F76 /* OrderedSet.swift */; }; @@ -37,6 +38,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 680759631FC5A1F600D6CA70 /* OptionsWithEnumProtocolSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsWithEnumProtocolSpec.swift; sourceTree = ""; }; 6CAD549C1D371A4E00A2D031 /* LinuxMain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LinuxMain.swift; path = Tests/LinuxMain.swift; sourceTree = SOURCE_ROOT; }; CD2ED3401C1E6C5D0076092B /* Argument.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Argument.swift; sourceTree = ""; }; CD2ED3421C1E6D540076092B /* ArgumentProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArgumentProtocol.swift; sourceTree = ""; }; @@ -146,6 +148,7 @@ children = ( CDFC88351C3C0612003AC8F8 /* CommandSpec.swift */, D00CCE281A20741C00109F8C /* OptionSpec.swift */, + 680759631FC5A1F600D6CA70 /* OptionsWithEnumProtocolSpec.swift */, CDCE78351FBAB0A2005A9F76 /* OrderedSetSpec.swift */, D00CCDE91A20717400109F8C /* Supporting Files */, 6CAD549C1D371A4E00A2D031 /* LinuxMain.swift */, @@ -381,8 +384,9 @@ buildActionMask = 2147483647; files = ( CDFC88361C3C0612003AC8F8 /* CommandSpec.swift in Sources */, - CDCE78361FBAB0A2005A9F76 /* OrderedSetSpec.swift in Sources */, D00CCE291A20741C00109F8C /* OptionSpec.swift in Sources */, + 680759651FC5A25D00D6CA70 /* OptionsWithEnumProtocolSpec.swift in Sources */, + CDCE78361FBAB0A2005A9F76 /* OrderedSetSpec.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/Commandant/ArgumentProtocol.swift b/Sources/Commandant/ArgumentProtocol.swift index 2b9a065..84d7f73 100644 --- a/Sources/Commandant/ArgumentProtocol.swift +++ b/Sources/Commandant/ArgumentProtocol.swift @@ -30,3 +30,33 @@ extension String: ArgumentProtocol { return string } } + +public extension RawRepresentable where Self.RawValue: StringProtocol, Self: ArgumentProtocol { + + public static func from(string: String) -> Self? { + + guard let stringValue = Self.RawValue(string) + else { + return .none + } + + return Self(rawValue: stringValue) + + } + +} + +public extension RawRepresentable where Self.RawValue: FixedWidthInteger, Self: ArgumentProtocol { + + public static func from(string: String) -> Self? { + + guard let intValue = Self.RawValue(string) + else { + return .none + } + + return Self(rawValue: intValue) + + } + +} diff --git a/Tests/CommandantTests/OptionsWithEnumProtocolSpec.swift b/Tests/CommandantTests/OptionsWithEnumProtocolSpec.swift new file mode 100644 index 0000000..aed8ca2 --- /dev/null +++ b/Tests/CommandantTests/OptionsWithEnumProtocolSpec.swift @@ -0,0 +1,183 @@ +// +// OptionsWithEnumProtocolSpec.swift +// Commandant +// +// Created by Vitalii Budnik on 11/22/17. +// Copyright © 2017 Carthage. All rights reserved. +// + +@testable import Commandant +import Foundation +import Nimble +import Quick +import Result + +class OptionsWithEnumProtocolSpec: QuickSpec { + override func spec() { + describe("CommandMode.Arguments") { + func tryArguments(_ arguments: String...) -> Result> { + return TestEnumOptions.evaluate(.arguments(ArgumentParser(arguments))) + } + + it("should fail if a required argument is missing") { + expect(tryArguments().value).to(beNil()) + } + + it("should fail if an option is missing a value") { + expect(tryArguments("required", "--strictIntValue").value).to(beNil()) + } + + it("should fail if an option is missing a value") { + expect(tryArguments("required", "--strictStringValue", "drop").value).to(beNil()) + } + + it("should fail if an optional strict int parameter is wrong") { + expect(tryArguments("required", "256").value).to(beNil()) + } + + it("should succeed without optional string arguments") { + let value = tryArguments("required").value + let expected = TestEnumOptions(strictIntValue: .theAnswerToTheUltimateQuestionOfLifeTheUniverseAndEverything, strictStringValue: .foobar, strictStringsArray: [], optionalStrictStringsArray: nil, optionalStrictStringValue: nil, optionalStrictInt: .min, requiredName: "required", arguments: []) + expect(value).to(equal(expected)) + } + + it("should succeed without optional strict int value") { + let value = tryArguments("required", "5").value + let expected = TestEnumOptions(strictIntValue: .theAnswerToTheUltimateQuestionOfLifeTheUniverseAndEverything, strictStringValue: .foobar, strictStringsArray: [], optionalStrictStringsArray: nil, optionalStrictStringValue: nil, optionalStrictInt: .giveFive, requiredName: "required", arguments: []) + expect(value).to(equal(expected)) + } + + it("should succeed with some strings array arguments separated by comma") { + let value = tryArguments("required", "--strictIntValue", "3", "--optionalStrictStringValue", "baz", "255", "--strictStringsArray", "a,b,c").value + let expected = TestEnumOptions(strictIntValue: .three, strictStringValue: .foobar, strictStringsArray: [.a, .b, .c], optionalStrictStringsArray: nil, optionalStrictStringValue: .baz, optionalStrictInt: .max, requiredName: "required", arguments: []) + expect(value).to(equal(expected)) + } + + it("should succeed with some strings array arguments separated by space") { + let value = tryArguments("required", "--strictIntValue", "3", "--optionalStrictStringValue", "baz", "--strictStringsArray", "a b c", "255").value + let expected = TestEnumOptions(strictIntValue: .three, strictStringValue: .foobar, strictStringsArray: [.a, .b, .c], optionalStrictStringsArray: nil, optionalStrictStringValue: .baz, optionalStrictInt: .max, requiredName: "required", arguments: []) + expect(value).to(equal(expected)) + } + + it("should succeed with some strings array arguments separated by comma and space") { + let value = tryArguments("required", "--strictIntValue", "3", "--optionalStrictStringValue", "baz", "--strictStringsArray", "a, b, c", "255").value + let expected = TestEnumOptions(strictIntValue: .three, strictStringValue: .foobar, strictStringsArray: [.a, .b, .c], optionalStrictStringsArray: nil, optionalStrictStringValue: .baz, optionalStrictInt: .max, requiredName: "required", arguments: []) + expect(value).to(equal(expected)) + } + + it("should succeed with some optional string arguments") { + let value = tryArguments("required", "--strictIntValue", "3", "--optionalStrictStringValue", "baz", "255").value + let expected = TestEnumOptions(strictIntValue: .three, strictStringValue: .foobar, strictStringsArray: [], optionalStrictStringsArray: nil, optionalStrictStringValue: .baz, optionalStrictInt: .max, requiredName: "required", arguments: []) + expect(value).to(equal(expected)) + } + + it("should succeed without optional array arguments") { + let value = tryArguments("required").value + let expected = TestEnumOptions(strictIntValue: .theAnswerToTheUltimateQuestionOfLifeTheUniverseAndEverything, strictStringValue: .foobar, strictStringsArray: [], optionalStrictStringsArray: nil, optionalStrictStringValue: nil, optionalStrictInt: .min, requiredName: "required", arguments: []) + expect(value).to(equal(expected)) + } + + it("should succeed with some optional array arguments") { + let value = tryArguments("required", "--strictIntValue", "3", "--optionalStrictStringsArray", "one, two", "255").value + let expected = TestEnumOptions(strictIntValue: .three, strictStringValue: .foobar, strictStringsArray: [], optionalStrictStringsArray: [.one, .two], optionalStrictStringValue: nil, optionalStrictInt: .max, requiredName: "required", arguments: []) + expect(value).to(equal(expected)) + } + + it("should override previous optional arguments") { + let value = tryArguments("required", "--strictIntValue", "3", "--strictStringValue", "fuzzbuzz", "--strictIntValue", "5", "--strictStringValue", "bazbuzz").value + let expected = TestEnumOptions(strictIntValue: .giveFive, strictStringValue: .bazbuzz, strictStringsArray: [], optionalStrictStringsArray: nil, optionalStrictStringValue: nil, optionalStrictInt: .min, requiredName: "required", arguments: []) + expect(value).to(equal(expected)) + } + + it("should consume the rest of positional arguments") { + let value = tryArguments("required", "255", "value1", "value2").value + let expected = TestEnumOptions(strictIntValue: .theAnswerToTheUltimateQuestionOfLifeTheUniverseAndEverything, strictStringValue: .foobar, strictStringsArray: [], optionalStrictStringsArray: nil, optionalStrictStringValue: nil, optionalStrictInt: .max, requiredName: "required", arguments: [ "value1", "value2" ]) + expect(value).to(equal(expected)) + } + + it("should treat -- as the end of valued options") { + let value = tryArguments("--", "--strictIntValue").value + let expected = TestEnumOptions(strictIntValue: .theAnswerToTheUltimateQuestionOfLifeTheUniverseAndEverything, strictStringValue: .foobar, strictStringsArray: [], optionalStrictStringsArray: nil, optionalStrictStringValue: nil, optionalStrictInt: .min, requiredName: "--strictIntValue", arguments: []) + expect(value).to(equal(expected)) + } + } + + describe("CommandMode.Usage") { + it("should return an error containing usage information") { + let error = TestEnumOptions.evaluate(.usage).error + expect(error?.description).to(contain("strictIntValue")) + expect(error?.description).to(contain("strictStringValue")) + expect(error?.description).to(contain("name you're required to")) + expect(error?.description).to(contain("optionally specify")) + } + } + } +} + +struct TestEnumOptions: OptionsProtocol, Equatable { + let strictIntValue: StrictIntValue + let strictStringValue: StrictStringValue + let strictStringsArray: [StrictStringValue] + let optionalStrictStringsArray: [StrictStringValue]? + let optionalStrictStringValue: StrictStringValue? + let optionalStrictInt: StrictIntValue + let requiredName: String + let arguments: [String] + + typealias ClientError = NoError + + static func create(_ a: StrictIntValue) -> (StrictStringValue) -> ([StrictStringValue]) -> ([StrictStringValue]?) -> (StrictStringValue?) -> (String) -> (StrictIntValue) -> ([String]) -> TestEnumOptions { + return { b in { c in { d in { e in { f in { g in { h in + return self.init(strictIntValue: a, strictStringValue: b, strictStringsArray: c, optionalStrictStringsArray: d, optionalStrictStringValue: e, optionalStrictInt: g, requiredName: f, arguments: h) + } } } } } } } + } + + static func evaluate(_ m: CommandMode) -> Result> { + return create + <*> m <| Option(key: "strictIntValue", defaultValue: .theAnswerToTheUltimateQuestionOfLifeTheUniverseAndEverything, usage: "`0` - zero, `255` - max, `3` - three, `5` - five or `42` - The Answer") + <*> m <| Option(key: "strictStringValue", defaultValue: .foobar, usage: "`foobar`, `bazbuzzz`, `a`, `b`, `c`, `one`, `two`, `c`") + <*> m <| Option<[StrictStringValue]>(key: "strictStringsArray", defaultValue: [], usage: "Some array of arguments") + <*> m <| Option<[StrictStringValue]?>(key: "optionalStrictStringsArray", defaultValue: nil, usage: "Some array of arguments") + <*> m <| Option(key: "optionalStrictStringValue", defaultValue: nil, usage: "Some string value") + <*> m <| Argument(usage: "A name you're required to specify") + <*> m <| Argument(defaultValue: .min, usage: "A number that you can optionally specify") + <*> m <| Argument(defaultValue: [], usage: "An argument list that consumes the rest of positional arguments") + } +} + +func ==(lhs: TestEnumOptions, rhs: TestEnumOptions) -> Bool { + return lhs.strictIntValue == rhs.strictIntValue && lhs.strictStringValue == rhs.strictStringValue && lhs.strictStringsArray == rhs.strictStringsArray && lhs.optionalStrictStringsArray == rhs.optionalStrictStringsArray && lhs.optionalStrictStringValue == rhs.optionalStrictStringValue && lhs.optionalStrictInt == rhs.optionalStrictInt && lhs.requiredName == rhs.requiredName && lhs.arguments == rhs.arguments +} + +extension TestEnumOptions: CustomStringConvertible { + var description: String { + return "{ strictIntValue: \(strictIntValue), strictStringValue: \(strictStringValue), strictStringsArray: \(strictStringsArray), optionalStrictStringsArray: \(String(describing: optionalStrictStringsArray)), optionalStrictStringValue: \(String(describing: optionalStrictStringValue)), optionalStrictInt: \(optionalStrictInt), requiredName: \(requiredName), arguments: \(arguments) }" + } +} + +enum StrictStringValue: String, ArgumentProtocol { + + static var name: String = "Strict string value: `foobar`, `bazbuzz`, `one`, `two`, `baz`, `a`, `b` or `c`" + + case foobar + case bazbuzz + case one + case two + case baz + case a + case b + case c + +} + +enum StrictIntValue: UInt8, ArgumentProtocol { + + static var name: String = "Strict int value: `3`, `5`, `42`, `0`, `255`" + + case min = 0 + case three = 3 + case giveFive = 5 + case theAnswerToTheUltimateQuestionOfLifeTheUniverseAndEverything = 42 + case max = 255 + +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 810295c..8b54e63 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -6,5 +6,6 @@ import Quick Quick.QCKMain([ CommandWrapperSpec.self, OptionsProtocolSpec.self, + OptionsWithEnumProtocolSpec.self, OrderedSetSpec.self, ])