Skip to content

Commit b6dcfc2

Browse files
committed
refactor: move URLRouting integration behind trait
- Add URLRouting trait to Package.swift - Make URLRouting dependency conditional on trait - Remove URLFormCodingURLRouting target and product - Move URLRouting integration code to Sources/URLFormCoding/Traits/URLRouting.swift - Guard all URLRouting code with #if URLRouting - Move trait-dependent tests to Tests/URLFormCoding Tests/Traits/ - Delete old URLFormCodingURLRouting directory - Enable URLRouting trait by default Breaking change: Import URLFormCoding instead of URLFormCodingURLRouting
1 parent f15932f commit b6dcfc2

File tree

6 files changed

+160
-1390
lines changed

6 files changed

+160
-1390
lines changed

Package.swift

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@ import PackageDescription
44

55
extension String {
66
static let urlFormCoding: Self = "URLFormCoding"
7-
static let urlFormCodingURLRouting: Self = "URLFormCodingURLRouting"
87
}
98

109
extension Target.Dependency {
1110
static var urlFormCoding: Self { .target(name: .urlFormCoding) }
12-
static var urlFormCodingURLRouting: Self { .target(name: .urlFormCodingURLRouting) }
1311
static var rfc2388: Self { .product(name: "RFC 2388", package: "swift-rfc-2388") }
1412
static var whatwgUrlEncoding: Self { .product(name: "WHATWG URL Encoding", package: "swift-whatwg-url-encoding") }
1513
static var urlRouting: Self { .product(name: "URLRouting", package: "swift-url-routing") }
@@ -24,11 +22,16 @@ let package = Package(
2422
.watchOS(.v10)
2523
],
2624
products: [
27-
.library(name: .urlFormCoding, targets: [.urlFormCoding]),
28-
.library(name: .urlFormCodingURLRouting, targets: [.urlFormCodingURLRouting])
25+
.library(name: .urlFormCoding, targets: [.urlFormCoding])
26+
],
27+
traits: [
28+
.trait(
29+
name: "URLRouting",
30+
description: "URLRouting integration for URLFormCoding"
31+
)
2932
],
3033
dependencies: [
31-
.package(path: "../../swift-standards/swift-rfc-2388"),
34+
.package(url: "https://github.com/swift-standards/swift-rfc-2388", from: "0.1.0"),
3235
.package(url: "https://github.com/swift-standards/swift-whatwg-url-encoding.git", from: "0.1.0"),
3336
.package(url: "https://github.com/pointfreeco/swift-url-routing", from: "0.6.0")
3437
],
@@ -37,30 +40,27 @@ let package = Package(
3740
name: .urlFormCoding,
3841
dependencies: [
3942
.rfc2388,
40-
.whatwgUrlEncoding
43+
.whatwgUrlEncoding,
44+
.product(
45+
name: "URLRouting",
46+
package: "swift-url-routing",
47+
condition: .when(traits: ["URLRouting"])
48+
)
4149
]
4250
),
4351
.testTarget(
4452
name: .urlFormCoding.tests,
4553
dependencies: [
4654
.urlFormCoding
4755
]
48-
),
49-
.target(
50-
name: .urlFormCodingURLRouting,
51-
dependencies: [
52-
.urlRouting,
53-
.urlFormCoding
54-
]
55-
),
56-
.testTarget(
57-
name: .urlFormCodingURLRouting.tests,
58-
dependencies: [
59-
.urlFormCoding,
60-
.urlFormCodingURLRouting
61-
]
6256
)
6357
]
6458
)
6559

60+
package.traits.insert(
61+
.default(
62+
enabledTraits: ["URLRouting"]
63+
)
64+
)
65+
6666
extension String { var tests: Self { self + " Tests" } }

Sources/URLFormCodingURLRouting/Form.Conversion.swift renamed to Sources/URLFormCoding/Traits/URLRouting.swift

Lines changed: 96 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
1-
//
2-
// File.swift
3-
// swift-url-form-coding-url-routing
4-
//
5-
// Created by Coen ten Thije Boonkkamp on 26/07/2025.
6-
//
7-
1+
#if URLRouting
82
import Foundation
9-
import URLFormCoding
10-
import URLRouting
3+
@_exported import URLRouting
4+
5+
// MARK: - Form.Conversion
116

127
/// A conversion that handles URL form data encoding and decoding for URLRouting.
138
///
14-
/// `FormCoding` provides seamless conversion between Swift Codable types and
9+
/// `Form.Conversion` provides seamless conversion between Swift Codable types and
1510
/// URL-encoded form data (application/x-www-form-urlencoded format). It's the
1611
/// standard format used by HTML forms and many web APIs.
1712
///
@@ -32,7 +27,7 @@ import URLRouting
3227
/// }
3328
///
3429
/// // Create form conversion
35-
/// let formConversion = FormCoding(ContactForm.self)
30+
/// let formConversion = Form.Conversion(ContactForm.self)
3631
///
3732
/// // Use in route definition
3833
/// Route {
@@ -55,7 +50,7 @@ import URLRouting
5550
/// encoder.dateEncodingStrategy = .iso8601
5651
/// encoder.arrayEncodingStrategy = .brackets
5752
///
58-
/// let advancedForm = FormCoding(
53+
/// let advancedForm = Form.Conversion(
5954
/// ContactForm.self,
6055
/// decoder: decoder,
6156
/// encoder: encoder
@@ -114,7 +109,7 @@ extension Form {
114109
/// encoder.arrayEncodingStrategy = .indexedBrackets
115110
/// encoder.dateEncodingStrategy = .secondsSince1970
116111
///
117-
/// let formCoding = FormCoding(
112+
/// let formCoding = Form.Conversion(
118113
/// MyModel.self,
119114
/// decoder: decoder,
120115
/// encoder: encoder
@@ -192,3 +187,91 @@ extension Form.Conversion: URLRouting.Conversion {
192187
try encoder.encode(output)
193188
}
194189
}
190+
191+
// MARK: - URLRouting.Conversion Extensions
192+
193+
extension URLRouting.Conversion {
194+
/// Creates a URL form data conversion for the specified Codable type.
195+
///
196+
/// This static method provides a convenient way to create ``Form.Conversion``
197+
/// instances for use in URLRouting route definitions. Form coding handles
198+
/// standard web form data (application/x-www-form-urlencoded).
199+
///
200+
/// - Parameters:
201+
/// - type: The Codable type to convert to/from form data
202+
/// - decoder: Optional custom URL form decoder (uses default if not provided)
203+
/// - encoder: Optional custom URL form encoder (uses default if not provided)
204+
/// - Returns: A ``Form.Conversion`` instance
205+
///
206+
/// ## Example
207+
///
208+
/// ```swift
209+
/// struct LoginRequest: Codable {
210+
/// let username: String
211+
/// let password: String
212+
/// }
213+
///
214+
/// // Create conversion with default encoder/decoder
215+
/// let loginConversion = Conversion.form(LoginRequest.self)
216+
///
217+
/// // Create conversion with custom configuration
218+
/// let decoder = Form.Decoder()
219+
/// decoder.parsingStrategy = .brackets
220+
/// let encoder = Form.Encoder()
221+
/// encoder.dateEncodingStrategy = .iso8601
222+
///
223+
/// let customConversion = Conversion.form(
224+
/// LoginRequest.self,
225+
/// decoder: decoder,
226+
/// encoder: encoder
227+
/// )
228+
/// ```
229+
///
230+
/// ## Usage in Routes
231+
///
232+
/// ```swift
233+
/// Route {
234+
/// Method.post
235+
/// Path { "login" }
236+
/// Body(.form(LoginRequest.self))
237+
/// }
238+
/// ```
239+
public static func form<Value>(
240+
_ type: Value.Type,
241+
decoder: Form.Decoder = .init(),
242+
encoder: Form.Encoder = .init()
243+
) -> Self where Self == Form.Conversion<Value> {
244+
.init(type, decoder: decoder, encoder: encoder)
245+
}
246+
247+
/// Maps this conversion through a URL form data conversion.
248+
///
249+
/// This method allows you to chain conversions, applying form data
250+
/// conversion after another conversion has been applied.
251+
///
252+
/// - Parameters:
253+
/// - type: The Codable type to convert to/from form data
254+
/// - decoder: Optional custom URL form decoder (uses default if not provided)
255+
/// - encoder: Optional custom URL form encoder (uses default if not provided)
256+
/// - Returns: A mapped conversion that applies both conversions in sequence
257+
///
258+
/// ## Example
259+
///
260+
/// ```swift
261+
/// struct APIRequest: Codable {
262+
/// let data: UserProfile
263+
/// }
264+
///
265+
/// // Chain conversions: first transform data, then apply form conversion
266+
/// let chainedConversion = Conversion<Data, Data>.identity
267+
/// .form(UserProfile.self)
268+
/// ```
269+
public func form<Value>(
270+
_ type: Value.Type,
271+
decoder: Form.Decoder = .init(),
272+
encoder: Form.Encoder = .init()
273+
) -> Conversions.Map<Self, Form.Conversion<Value>> {
274+
self.map(.form(type, decoder: decoder, encoder: encoder))
275+
}
276+
}
277+
#endif

Sources/URLFormCodingURLRouting/URLRouting.Conversion.swift

Lines changed: 0 additions & 88 deletions
This file was deleted.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#if URLRouting
2+
import Testing
3+
import Foundation
4+
import URLRouting
5+
import URLFormCoding
6+
7+
@Suite("Form.Conversion Integration Tests")
8+
struct FormConversionIntegrationTests {
9+
10+
struct TestRequest: Codable, Equatable {
11+
let name: String
12+
let age: Int
13+
}
14+
15+
@Test("Form.Conversion exists and is accessible")
16+
func testConversionExists() throws {
17+
let conversion = Form.Conversion(TestRequest.self)
18+
let request = TestRequest(name: "Test", age: 25)
19+
// Just verify it can encode
20+
let data = try conversion.unapply(request)
21+
#expect(!data.isEmpty)
22+
}
23+
24+
@Test("URLRouting.Conversion.form() static method works")
25+
func testStaticFormMethod() throws {
26+
// Use explicit type to call static method on concrete type
27+
let conversion: Form.Conversion<TestRequest> = .form(TestRequest.self)
28+
let request = TestRequest(name: "John", age: 30)
29+
let data = try conversion.unapply(request)
30+
#expect(!data.isEmpty)
31+
}
32+
33+
@Test("Round-trip encoding and decoding")
34+
func testRoundTrip() throws {
35+
let conversion = Form.Conversion(TestRequest.self)
36+
let original = TestRequest(name: "Jane", age: 25)
37+
38+
let encoded = try conversion.unapply(original)
39+
let decoded = try conversion.apply(encoded)
40+
41+
#expect(decoded == original)
42+
}
43+
}
44+
#endif

0 commit comments

Comments
 (0)