Skip to content

Commit 0749954

Browse files
committed
Add Positional
feature/add-positional-and-option-set
1 parent 447f0c0 commit 0749954

File tree

5 files changed

+412
-13
lines changed

5 files changed

+412
-13
lines changed

README.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,21 @@ struct MyCommand: TopLevelCommandRepresentable {
5151
}
5252
```
5353

54+
Positional arguments that are just a value, with no key are supported through the `Positional` type.
55+
56+
```swift
57+
struct MyCommand: TopLevelCommandRepresentable {
58+
func commandValue() -> Command { "my-command" }
59+
var flagFormatter: FlagFormatter { .doubleDashPrefix }
60+
var optionFormatter: OptionFormatter { .doubleDashPrefix }
61+
62+
@Flag var myFlag: Bool = false
63+
@Option var myOption: Int = 0
64+
@OptionSet var myOptions: [String] = ["value1", "value2"]
65+
@Positional var myPositional: String = "positional"
66+
}
67+
```
68+
5469
## Motivation
5570

5671
When running executables with Swift, it may be helpful to encode structured types (struct, class, enum) into argument arrays that are passed to executables.
@@ -126,12 +141,12 @@ struct RunCommand: CommandRepresentable {
126141
let flagFormatter: FlagFormatter = .doubleDashPrefixKebabCase
127142
let optionFormatter: OptionFormatter = .doubleDashPrefixKebabCase
128143

129-
let executable: Command
144+
@Positional var executable: String
130145
}
131146

132147
extension RunCommand: ExpressibleByStringLiteral {
133148
init(stringLiteral value: StringLiteralType) {
134-
self.init(executable: Command(rawValue: value))
149+
self.init(executable: Positional(wrapped: value))
135150
}
136151
}
137152

Sources/ArgumentEncoding/ArgumentGroup.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ extension ArgumentGroup {
112112
return .commandRep(commandRep)
113113
} else if let group = value as? (any ArgumentGroup) {
114114
return .group(group)
115+
} else if let positional = value as? PositionalProtocol {
116+
return .positional(positional)
115117
} else {
116118
return nil
117119
}
@@ -140,6 +142,8 @@ extension ArgumentGroup {
140142
}
141143
case let .group(group):
142144
return group.arguments()
145+
case let .positional(positional):
146+
return positional.arguments()
143147
}
144148
})
145149
}
@@ -260,4 +264,5 @@ private enum Container {
260264
case topLevelCommandRep(any TopLevelCommandRepresentable)
261265
case commandRep(any CommandRepresentable)
262266
case group(any ArgumentGroup)
267+
case positional(any PositionalProtocol)
263268
}
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
// PositionalArgument.swift
2+
// ArgumentEncoding
3+
//
4+
// Copyright © 2023 MFB Technologies, Inc. All rights reserved.
5+
6+
import Foundation
7+
8+
/// A value only argument type that is not a command or sub-command.
9+
///
10+
/// Because positional argumnents do not have a key, they encode to only their value..
11+
///
12+
/// ```swift
13+
/// struct Container: ArgumentGroup, FormatterNode {
14+
/// let flagFormatter: FlagFormatter = .doubleDashPrefix
15+
/// let optionFormatter: OptionFormatter = .doubleDashPrefix
16+
///
17+
/// @Positional var name: String = "value"
18+
/// }
19+
///
20+
/// Container().arguments() == ["value"]
21+
/// ```
22+
@propertyWrapper
23+
public struct Positional<Value>: PositionalProtocol {
24+
public var wrappedValue: Value
25+
26+
// Different Value types will encode to arguments differently.
27+
// Using unwrap, this can be handled individually per type or collectively by protocol
28+
private let unwrap: @Sendable (Value) -> [String]
29+
internal var unwrapped: [String] {
30+
unwrap(wrappedValue)
31+
}
32+
33+
func encoding() -> [Command] {
34+
unwrapped.map(Command.init(rawValue:))
35+
}
36+
37+
/// Get the Positional's argument encoding.
38+
/// - Returns: The argument encoding which is an array of strings
39+
public func arguments() -> [String] {
40+
encoding().flatMap { $0.arguments() }
41+
}
42+
43+
/// Initializes a new positional when not used as a `@propertyWrapper`
44+
///
45+
/// - Parameters
46+
/// - wrappedValue: The underlying value
47+
/// - unwrap: A closure for mapping a Value to [String]
48+
public init(value: Value, unwrap: @escaping @Sendable (Value) -> [String]) {
49+
wrappedValue = value
50+
self.unwrap = unwrap
51+
}
52+
53+
/// Initializes a new positional when used as a `@propertyWrapper`
54+
///
55+
/// - Parameters
56+
/// - wrappedValue: The underlying value
57+
/// - _ unwrap: A closure for mapping a Value to [String]
58+
public init(wrappedValue: Value, _ unwrap: @escaping @Sendable (Value) -> [String]) {
59+
self.wrappedValue = wrappedValue
60+
self.unwrap = unwrap
61+
}
62+
}
63+
64+
// MARK: Conditional Conformances
65+
66+
extension Positional: Equatable where Value: Equatable {
67+
public static func == (lhs: Positional<Value>, rhs: Positional<Value>) -> Bool {
68+
lhs.unwrapped == rhs.unwrapped
69+
}
70+
}
71+
72+
extension Positional: Hashable where Value: Hashable {
73+
public func hash(into hasher: inout Hasher) {
74+
hasher.combine(unwrapped)
75+
hasher.combine(ObjectIdentifier(Self.self))
76+
}
77+
}
78+
79+
extension Positional: Sendable where Value: Sendable {}
80+
81+
// MARK: Convenience initializers when Value: CustomStringConvertible
82+
83+
extension Positional where Value: CustomStringConvertible {
84+
/// Initializes a new positional when not used as a `@propertyWrapper`
85+
///
86+
/// - Parameters
87+
/// - wrappedValue: The underlying value
88+
public init(value: Value) {
89+
wrappedValue = value
90+
unwrap = Self.unwrap(_:)
91+
}
92+
93+
/// Initializes a new positional when used as a `@propertyWrapper`
94+
///
95+
/// - Parameters
96+
/// - wrappedValue: The underlying value
97+
public init(wrappedValue: Value) {
98+
self.wrappedValue = wrappedValue
99+
unwrap = Self.unwrap(_:)
100+
}
101+
102+
@Sendable
103+
public static func unwrap(_ value: Value) -> [String] {
104+
[value.description]
105+
}
106+
}
107+
108+
extension Positional where Value: RawRepresentable, Value.RawValue: CustomStringConvertible {
109+
/// Initializes a new positional when not used as a `@propertyWrapper`
110+
///
111+
/// - Parameters
112+
/// - wrappedValue: The underlying value
113+
public init(value: Value) {
114+
wrappedValue = value
115+
unwrap = Self.unwrap(_:)
116+
}
117+
118+
/// Initializes a new positional when used as a `@propertyWrapper`
119+
///
120+
/// - Parameters
121+
/// - wrappedValue: The underlying value
122+
public init(wrappedValue: Value) {
123+
self.wrappedValue = wrappedValue
124+
unwrap = Self.unwrap(_:)
125+
}
126+
127+
@Sendable
128+
public static func unwrap(_ value: Value) -> [String] {
129+
[value.rawValue.description]
130+
}
131+
}
132+
133+
extension Positional where Value: CustomStringConvertible, Value: RawRepresentable,
134+
Value.RawValue: CustomStringConvertible
135+
{
136+
/// Initializes a new positional when not used as a `@propertyWrapper`
137+
///
138+
/// - Parameters
139+
/// - wrappedValue: The underlying value
140+
public init(value: Value) {
141+
wrappedValue = value
142+
unwrap = Self.unwrap(_:)
143+
}
144+
145+
/// Initializes a new positional when used as a `@propertyWrapper`
146+
///
147+
/// - Parameters
148+
/// - wrappedValue: The underlying value
149+
public init(wrappedValue: Value) {
150+
self.wrappedValue = wrappedValue
151+
unwrap = Self.unwrap(_:)
152+
}
153+
154+
@Sendable
155+
public static func unwrap(_ value: Value) -> [String] {
156+
[value.rawValue.description]
157+
}
158+
}
159+
160+
// MARK: Convenience initializers when Value == Positionalal<Wrapped>
161+
162+
extension Positional {
163+
/// Initializes a new positional when not used as a `@propertyWrapper`
164+
///
165+
/// - Parameters
166+
/// - wrappedValue: The underlying value
167+
public init<Wrapped>(value: Wrapped?) where Wrapped: CustomStringConvertible,
168+
Value == Wrapped?
169+
{
170+
wrappedValue = value
171+
unwrap = Self.unwrap(_:)
172+
}
173+
174+
/// Initializes a new positional when used as a `@propertyWrapper`
175+
///
176+
/// - Parameters
177+
/// - wrappedValue: The underlying value
178+
public init<Wrapped>(wrappedValue: Wrapped?) where Wrapped: CustomStringConvertible,
179+
Value == Wrapped?
180+
{
181+
self.wrappedValue = wrappedValue
182+
unwrap = Self.unwrap(_:)
183+
}
184+
185+
@Sendable
186+
public static func unwrap<Wrapped>(_ value: Wrapped?) -> [String] where Wrapped: CustomStringConvertible,
187+
Value == Wrapped?
188+
{
189+
[value?.description].compactMap { $0 }
190+
}
191+
}
192+
193+
// MARK: Convenience initializers when Value == Sequence<E>
194+
195+
extension Positional {
196+
/// Initializes a new positional when not used as a `@propertyWrapper`
197+
///
198+
/// - Parameters
199+
/// - wrappedValue: The underlying value
200+
public init<E>(values: Value) where Value: Sequence, Value.Element == E,
201+
E: CustomStringConvertible
202+
{
203+
wrappedValue = values
204+
unwrap = Self.unwrap(_:)
205+
}
206+
207+
/// Initializes a new positional when used as a `@propertyWrapper`
208+
///
209+
/// - Parameters
210+
/// - wrappedValue: The underlying value
211+
public init<E>(wrappedValue: Value) where Value: Sequence, Value.Element == E,
212+
E: CustomStringConvertible
213+
{
214+
self.wrappedValue = wrappedValue
215+
unwrap = Self.unwrap(_:)
216+
}
217+
218+
@Sendable
219+
public static func unwrap<E>(_ value: Value) -> [String] where Value: Sequence, Value.Element == E,
220+
E: CustomStringConvertible
221+
{
222+
value.map(\E.description)
223+
}
224+
}
225+
226+
// MARK: ExpressibleBy...Literal conformances
227+
228+
extension Positional: ExpressibleByIntegerLiteral where Value: BinaryInteger, Value.IntegerLiteralType == Int {
229+
public init(integerLiteral value: IntegerLiteralType) {
230+
self.init(wrappedValue: Value(integerLiteral: value)) { [$0.description] }
231+
}
232+
}
233+
234+
#if os(macOS)
235+
extension Positional: ExpressibleByFloatLiteral where Value: BinaryFloatingPoint {
236+
public init(floatLiteral value: FloatLiteralType) {
237+
self.init(wrappedValue: Value(value)) { [$0.formatted()] }
238+
}
239+
}
240+
#endif
241+
242+
extension Positional: ExpressibleByExtendedGraphemeClusterLiteral where Value: StringProtocol {
243+
public init(extendedGraphemeClusterLiteral value: String) {
244+
self.init(wrappedValue: Value(stringLiteral: value))
245+
}
246+
}
247+
248+
extension Positional: ExpressibleByUnicodeScalarLiteral where Value: StringProtocol {
249+
public init(unicodeScalarLiteral value: String) {
250+
self.init(wrappedValue: Value(stringLiteral: value))
251+
}
252+
}
253+
254+
extension Positional: ExpressibleByStringLiteral where Value: StringProtocol {
255+
public init(stringLiteral value: StringLiteralType) {
256+
self.init(wrappedValue: Value(stringLiteral: value))
257+
}
258+
}
259+
260+
extension Positional: ExpressibleByStringInterpolation where Value: StringProtocol {
261+
public init(stringInterpolation: DefaultStringInterpolation) {
262+
self.init(wrappedValue: Value(stringInterpolation: stringInterpolation))
263+
}
264+
}
265+
266+
extension Positional: DecodableWithConfiguration where Value: Decodable {
267+
public init(from decoder: Decoder, configuration: @escaping @Sendable (Value) -> [String]) throws {
268+
let container = try decoder.singleValueContainer()
269+
try self.init(wrappedValue: container.decode(Value.self), configuration)
270+
}
271+
}
272+
273+
// MARK: Coding
274+
275+
extension Positional: Decodable where Value: Decodable {
276+
public init(from decoder: Decoder) throws {
277+
let container = try decoder.singleValueContainer()
278+
guard let configurationCodingUserInfoKey = Self.configurationCodingUserInfoKey(for: Value.Type.self) else {
279+
throw DecodingError.dataCorrupted(DecodingError.Context(
280+
codingPath: decoder.codingPath,
281+
debugDescription: "No CodingUserInfoKey found for accessing the DecodingConfiguration.",
282+
underlyingError: nil
283+
))
284+
}
285+
guard let _configuration = decoder.userInfo[configurationCodingUserInfoKey] else {
286+
throw DecodingError.dataCorrupted(DecodingError.Context(
287+
codingPath: decoder.codingPath,
288+
debugDescription: "No DecodingConfiguration found for key: \(configurationCodingUserInfoKey.rawValue)",
289+
underlyingError: nil
290+
))
291+
}
292+
guard let configuration = _configuration as? Self.DecodingConfiguration else {
293+
let desc = "Invalid DecodingConfiguration found for key: \(configurationCodingUserInfoKey.rawValue)"
294+
throw DecodingError.dataCorrupted(DecodingError.Context(
295+
codingPath: decoder.codingPath,
296+
debugDescription: desc,
297+
underlyingError: nil
298+
))
299+
}
300+
try self.init(wrappedValue: container.decode(Value.self), configuration)
301+
}
302+
303+
public static func configurationCodingUserInfoKey(for _: (some Any).Type) -> CodingUserInfoKey? {
304+
CodingUserInfoKey(rawValue: ObjectIdentifier(Self.self).debugDescription)
305+
}
306+
}
307+
308+
extension Positional: Encodable where Value: Encodable {
309+
public func encode(to encoder: Encoder) throws {
310+
var container = encoder.singleValueContainer()
311+
try container.encode(wrappedValue)
312+
}
313+
}
314+
315+
// MARK: Internal Types
316+
317+
/*
318+
Since Positional is generic, we need a single type to cast to in ArgumentGroup.
319+
PositionalProtocol is that type and Positional is the only type that conforms.
320+
*/
321+
protocol PositionalProtocol {
322+
func arguments() -> [String]
323+
}

0 commit comments

Comments
 (0)