Skip to content

Commit dcbc7bf

Browse files
authored
Use string representation for CIDR and IPAddress types (#458)
This change adds extension to the CIDR and IPAddress types to implement custom encode/decode functions for `Codable` conformance to use their string representation as the output from encode and input to decode. This would make the output from encoding this type (e.g. JSON) more human-readable rather than using the internal integer representation.
1 parent 528f635 commit dcbc7bf

File tree

7 files changed

+181
-4
lines changed

7 files changed

+181
-4
lines changed

Sources/ContainerizationExtras/CIDRv4.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
/// Describes an IPv4 CIDR address block.
1818
@frozen
19-
public struct CIDRv4: CustomStringConvertible, Equatable, Sendable, Hashable, Codable {
19+
public struct CIDRv4: CustomStringConvertible, Equatable, Sendable, Hashable {
2020
/// The IP component of this CIDR address.
2121
public let address: IPv4Address
2222

@@ -99,3 +99,16 @@ public struct CIDRv4: CustomStringConvertible, Equatable, Sendable, Hashable, Co
9999
"\(address)/\(prefix)"
100100
}
101101
}
102+
103+
extension CIDRv4: Codable {
104+
public init(from decoder: Decoder) throws {
105+
let container = try decoder.singleValueContainer()
106+
let string = try container.decode(String.self)
107+
try self.init(string)
108+
}
109+
110+
public func encode(to encoder: Encoder) throws {
111+
var container = encoder.singleValueContainer()
112+
try container.encode(description)
113+
}
114+
}

Sources/ContainerizationExtras/CIDRv6.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
/// Describes an IPv4 or IPv6 CIDR address block.
1818
@frozen
19-
public struct CIDRv6: CustomStringConvertible, Equatable, Sendable, Hashable, Codable {
19+
public struct CIDRv6: CustomStringConvertible, Equatable, Sendable, Hashable {
2020

2121
/// The IP component of this CIDR address.
2222
public let address: IPv6Address
@@ -100,3 +100,16 @@ public struct CIDRv6: CustomStringConvertible, Equatable, Sendable, Hashable, Co
100100
"\(address)/\(prefix)"
101101
}
102102
}
103+
104+
extension CIDRv6: Codable {
105+
public init(from decoder: Decoder) throws {
106+
let container = try decoder.singleValueContainer()
107+
let string = try container.decode(String.self)
108+
try self.init(string)
109+
}
110+
111+
public func encode(to encoder: Encoder) throws {
112+
var container = encoder.singleValueContainer()
113+
try container.encode(description)
114+
}
115+
}

Sources/ContainerizationExtras/IPv4Address.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
//===----------------------------------------------------------------------===//
1616

1717
@frozen
18-
public struct IPv4Address: Sendable, Hashable, CustomStringConvertible, Equatable, Comparable, Codable {
18+
public struct IPv4Address: Sendable, Hashable, CustomStringConvertible, Equatable, Comparable {
1919
public let value: UInt32
2020

2121
@inlinable
@@ -215,3 +215,16 @@ public struct IPv4Address: Sendable, Hashable, CustomStringConvertible, Equatabl
215215
lhs.value < rhs.value
216216
}
217217
}
218+
219+
extension IPv4Address: Codable {
220+
public init(from decoder: Decoder) throws {
221+
let container = try decoder.singleValueContainer()
222+
let string = try container.decode(String.self)
223+
try self.init(string)
224+
}
225+
226+
public func encode(to encoder: Encoder) throws {
227+
var container = encoder.singleValueContainer()
228+
try container.encode(description)
229+
}
230+
}

Sources/ContainerizationExtras/IPv6Address.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
//===----------------------------------------------------------------------===//
1616

1717
/// Represents an IPv6 network address conforming to RFC 5952 and RFC 4291.
18-
public struct IPv6Address: Sendable, Hashable, CustomStringConvertible, Equatable, Comparable, Codable {
18+
public struct IPv6Address: Sendable, Hashable, CustomStringConvertible, Equatable, Comparable {
1919
public let value: UInt128
2020

2121
public let zone: String?
@@ -252,3 +252,16 @@ public struct IPv6Address: Sendable, Hashable, CustomStringConvertible, Equatabl
252252
return (lhs.zone ?? "") < (rhs.zone ?? "")
253253
}
254254
}
255+
256+
extension IPv6Address: Codable {
257+
public init(from decoder: Decoder) throws {
258+
let container = try decoder.singleValueContainer()
259+
let string = try container.decode(String.self)
260+
try self.init(string)
261+
}
262+
263+
public func encode(to encoder: Encoder) throws {
264+
var container = encoder.singleValueContainer()
265+
try container.encode(description)
266+
}
267+
}

Tests/ContainerizationExtrasTests/TestCIDR.swift

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
// limitations under the License.
1515
//===----------------------------------------------------------------------===//
1616

17+
import Foundation
1718
import Testing
1819

1920
@testable import ContainerizationExtras
@@ -308,4 +309,66 @@ struct TestCIDR {
308309
let cidr = try CIDR("192.168.1.100/24")
309310
#expect(cidr.description == "192.168.1.100/24")
310311
}
312+
313+
@Test(
314+
"CIDRv4 Codable encodes to string representation",
315+
arguments: [
316+
"192.168.1.0/24",
317+
"10.0.0.0/8",
318+
"172.16.0.0/12",
319+
]
320+
)
321+
func testCIDRv4CodableEncode(cidr: String) throws {
322+
let original = try CIDRv4(cidr)
323+
let encoded = try JSONEncoder().encode(original)
324+
let jsonString = String(data: encoded, encoding: .utf8)!
325+
#expect(jsonString.contains(original.address.description))
326+
#expect(jsonString.contains("\(original.prefix.length)"))
327+
}
328+
329+
@Test(
330+
"CIDRv4 Codable decodes from string representation",
331+
arguments: [
332+
"192.168.1.0/24",
333+
"10.0.0.0/8",
334+
"172.16.0.0/12",
335+
]
336+
)
337+
func testCIDRv4CodableDecode(cidr: String) throws {
338+
let json = Data("\"\(cidr)\"".utf8)
339+
let decoded = try JSONDecoder().decode(CIDRv4.self, from: json)
340+
let expected = try CIDRv4(cidr)
341+
#expect(decoded == expected)
342+
}
343+
344+
@Test(
345+
"CIDRv6 Codable encodes to string representation",
346+
arguments: [
347+
("2001:db8::/32", "2001:db8::", 32),
348+
("fe80::/10", "fe80::", 10),
349+
("::1/128", "::1", 128),
350+
]
351+
)
352+
func testCIDRv6CodableEncode(cidr: String, expectedAddr: String, expectedPrefix: UInt8) throws {
353+
let original = try CIDRv6(cidr)
354+
let encoded = try JSONEncoder().encode(original)
355+
let jsonString = String(data: encoded, encoding: .utf8)!
356+
#expect(jsonString.contains(expectedAddr))
357+
#expect(jsonString.contains("\(expectedPrefix)"))
358+
}
359+
360+
@Test(
361+
"CIDRv6 Codable decodes from string representation",
362+
arguments: [
363+
"2001:db8::/32",
364+
"fe80::/10",
365+
"::1/128",
366+
]
367+
)
368+
func testCIDRv6CodableDecode(cidr: String) throws {
369+
let json = Data("\"\(cidr)\"".utf8)
370+
let decoded = try JSONDecoder().decode(CIDRv6.self, from: json)
371+
let expected = try CIDRv6(cidr)
372+
#expect(decoded == expected)
373+
}
311374
}

Tests/ContainerizationExtrasTests/TestIPv4Address.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,5 +427,36 @@ struct IPv4AddressTests {
427427
#expect(Bool(false), "Should have thrown IPAddressError, got: \(error)")
428428
}
429429
}
430+
431+
@Test(
432+
"Codable encodes to string representation",
433+
arguments: [
434+
"127.0.0.1",
435+
"192.168.1.1",
436+
"0.0.0.0",
437+
"255.255.255.255",
438+
]
439+
)
440+
func testCodableEncode(address: String) throws {
441+
let original = try IPv4Address(address)
442+
let encoded = try JSONEncoder().encode(original)
443+
#expect(String(data: encoded, encoding: .utf8) == "\"\(address)\"")
444+
}
445+
446+
@Test(
447+
"Codable decodes from string representation",
448+
arguments: [
449+
"127.0.0.1",
450+
"192.168.1.1",
451+
"0.0.0.0",
452+
"255.255.255.255",
453+
]
454+
)
455+
func testCodableDecode(address: String) throws {
456+
let json = Data("\"\(address)\"".utf8)
457+
let decoded = try JSONDecoder().decode(IPv4Address.self, from: json)
458+
let expected = try IPv4Address(address)
459+
#expect(decoded == expected)
460+
}
430461
}
431462
}

Tests/ContainerizationExtrasTests/TestIPv6Address.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,4 +240,35 @@ struct IPv6AddressTests {
240240
"Address \(addressString) (\(description)) should\(expected ? "" : " not") be documentation"
241241
)
242242
}
243+
244+
@Test(
245+
"Codable encodes to string representation",
246+
arguments: [
247+
("::1", "::1"),
248+
("2001:db8::1", "2001:db8::1"),
249+
("::", "::"),
250+
("fe80::1", "fe80::1"),
251+
]
252+
)
253+
func testCodableEncode(input: String, expected: String) throws {
254+
let original = try IPv6Address(input)
255+
let encoded = try JSONEncoder().encode(original)
256+
#expect(String(data: encoded, encoding: .utf8) == "\"\(expected)\"")
257+
}
258+
259+
@Test(
260+
"Codable decodes from string representation",
261+
arguments: [
262+
"::1",
263+
"2001:db8::1",
264+
"::",
265+
"fe80::1",
266+
]
267+
)
268+
func testCodableDecode(address: String) throws {
269+
let json = Data("\"\(address)\"".utf8)
270+
let decoded = try JSONDecoder().decode(IPv6Address.self, from: json)
271+
let expected = try IPv6Address(address)
272+
#expect(decoded == expected)
273+
}
243274
}

0 commit comments

Comments
 (0)