From a27bb27a8c6420f6bb1077c24b205bc9b3188e5e Mon Sep 17 00:00:00 2001 From: Yibo Zhuang Date: Sat, 20 Dec 2025 12:45:34 -0800 Subject: [PATCH] Use string representation for CIDR and IPAddress types 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. --- Sources/ContainerizationExtras/CIDRv4.swift | 15 ++++- Sources/ContainerizationExtras/CIDRv6.swift | 15 ++++- .../ContainerizationExtras/IPv4Address.swift | 15 ++++- .../ContainerizationExtras/IPv6Address.swift | 15 ++++- .../TestCIDR.swift | 63 +++++++++++++++++++ .../TestIPv4Address.swift | 31 +++++++++ .../TestIPv6Address.swift | 31 +++++++++ 7 files changed, 181 insertions(+), 4 deletions(-) diff --git a/Sources/ContainerizationExtras/CIDRv4.swift b/Sources/ContainerizationExtras/CIDRv4.swift index 7bc7c562..f676e90c 100644 --- a/Sources/ContainerizationExtras/CIDRv4.swift +++ b/Sources/ContainerizationExtras/CIDRv4.swift @@ -16,7 +16,7 @@ /// Describes an IPv4 CIDR address block. @frozen -public struct CIDRv4: CustomStringConvertible, Equatable, Sendable, Hashable, Codable { +public struct CIDRv4: CustomStringConvertible, Equatable, Sendable, Hashable { /// The IP component of this CIDR address. public let address: IPv4Address @@ -99,3 +99,16 @@ public struct CIDRv4: CustomStringConvertible, Equatable, Sendable, Hashable, Co "\(address)/\(prefix)" } } + +extension CIDRv4: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let string = try container.decode(String.self) + try self.init(string) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(description) + } +} diff --git a/Sources/ContainerizationExtras/CIDRv6.swift b/Sources/ContainerizationExtras/CIDRv6.swift index 32f15b74..1b2802d4 100644 --- a/Sources/ContainerizationExtras/CIDRv6.swift +++ b/Sources/ContainerizationExtras/CIDRv6.swift @@ -16,7 +16,7 @@ /// Describes an IPv4 or IPv6 CIDR address block. @frozen -public struct CIDRv6: CustomStringConvertible, Equatable, Sendable, Hashable, Codable { +public struct CIDRv6: CustomStringConvertible, Equatable, Sendable, Hashable { /// The IP component of this CIDR address. public let address: IPv6Address @@ -100,3 +100,16 @@ public struct CIDRv6: CustomStringConvertible, Equatable, Sendable, Hashable, Co "\(address)/\(prefix)" } } + +extension CIDRv6: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let string = try container.decode(String.self) + try self.init(string) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(description) + } +} diff --git a/Sources/ContainerizationExtras/IPv4Address.swift b/Sources/ContainerizationExtras/IPv4Address.swift index f8cf5be0..e7c6723d 100644 --- a/Sources/ContainerizationExtras/IPv4Address.swift +++ b/Sources/ContainerizationExtras/IPv4Address.swift @@ -15,7 +15,7 @@ //===----------------------------------------------------------------------===// @frozen -public struct IPv4Address: Sendable, Hashable, CustomStringConvertible, Equatable, Comparable, Codable { +public struct IPv4Address: Sendable, Hashable, CustomStringConvertible, Equatable, Comparable { public let value: UInt32 @inlinable @@ -215,3 +215,16 @@ public struct IPv4Address: Sendable, Hashable, CustomStringConvertible, Equatabl lhs.value < rhs.value } } + +extension IPv4Address: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let string = try container.decode(String.self) + try self.init(string) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(description) + } +} diff --git a/Sources/ContainerizationExtras/IPv6Address.swift b/Sources/ContainerizationExtras/IPv6Address.swift index ac140eb7..7d4264d0 100644 --- a/Sources/ContainerizationExtras/IPv6Address.swift +++ b/Sources/ContainerizationExtras/IPv6Address.swift @@ -15,7 +15,7 @@ //===----------------------------------------------------------------------===// /// Represents an IPv6 network address conforming to RFC 5952 and RFC 4291. -public struct IPv6Address: Sendable, Hashable, CustomStringConvertible, Equatable, Comparable, Codable { +public struct IPv6Address: Sendable, Hashable, CustomStringConvertible, Equatable, Comparable { public let value: UInt128 public let zone: String? @@ -252,3 +252,16 @@ public struct IPv6Address: Sendable, Hashable, CustomStringConvertible, Equatabl return (lhs.zone ?? "") < (rhs.zone ?? "") } } + +extension IPv6Address: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let string = try container.decode(String.self) + try self.init(string) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(description) + } +} diff --git a/Tests/ContainerizationExtrasTests/TestCIDR.swift b/Tests/ContainerizationExtrasTests/TestCIDR.swift index 086b90bf..68c39e37 100644 --- a/Tests/ContainerizationExtrasTests/TestCIDR.swift +++ b/Tests/ContainerizationExtrasTests/TestCIDR.swift @@ -14,6 +14,7 @@ // limitations under the License. //===----------------------------------------------------------------------===// +import Foundation import Testing @testable import ContainerizationExtras @@ -308,4 +309,66 @@ struct TestCIDR { let cidr = try CIDR("192.168.1.100/24") #expect(cidr.description == "192.168.1.100/24") } + + @Test( + "CIDRv4 Codable encodes to string representation", + arguments: [ + "192.168.1.0/24", + "10.0.0.0/8", + "172.16.0.0/12", + ] + ) + func testCIDRv4CodableEncode(cidr: String) throws { + let original = try CIDRv4(cidr) + let encoded = try JSONEncoder().encode(original) + let jsonString = String(data: encoded, encoding: .utf8)! + #expect(jsonString.contains(original.address.description)) + #expect(jsonString.contains("\(original.prefix.length)")) + } + + @Test( + "CIDRv4 Codable decodes from string representation", + arguments: [ + "192.168.1.0/24", + "10.0.0.0/8", + "172.16.0.0/12", + ] + ) + func testCIDRv4CodableDecode(cidr: String) throws { + let json = Data("\"\(cidr)\"".utf8) + let decoded = try JSONDecoder().decode(CIDRv4.self, from: json) + let expected = try CIDRv4(cidr) + #expect(decoded == expected) + } + + @Test( + "CIDRv6 Codable encodes to string representation", + arguments: [ + ("2001:db8::/32", "2001:db8::", 32), + ("fe80::/10", "fe80::", 10), + ("::1/128", "::1", 128), + ] + ) + func testCIDRv6CodableEncode(cidr: String, expectedAddr: String, expectedPrefix: UInt8) throws { + let original = try CIDRv6(cidr) + let encoded = try JSONEncoder().encode(original) + let jsonString = String(data: encoded, encoding: .utf8)! + #expect(jsonString.contains(expectedAddr)) + #expect(jsonString.contains("\(expectedPrefix)")) + } + + @Test( + "CIDRv6 Codable decodes from string representation", + arguments: [ + "2001:db8::/32", + "fe80::/10", + "::1/128", + ] + ) + func testCIDRv6CodableDecode(cidr: String) throws { + let json = Data("\"\(cidr)\"".utf8) + let decoded = try JSONDecoder().decode(CIDRv6.self, from: json) + let expected = try CIDRv6(cidr) + #expect(decoded == expected) + } } diff --git a/Tests/ContainerizationExtrasTests/TestIPv4Address.swift b/Tests/ContainerizationExtrasTests/TestIPv4Address.swift index f8de5020..c97027e5 100644 --- a/Tests/ContainerizationExtrasTests/TestIPv4Address.swift +++ b/Tests/ContainerizationExtrasTests/TestIPv4Address.swift @@ -427,5 +427,36 @@ struct IPv4AddressTests { #expect(Bool(false), "Should have thrown IPAddressError, got: \(error)") } } + + @Test( + "Codable encodes to string representation", + arguments: [ + "127.0.0.1", + "192.168.1.1", + "0.0.0.0", + "255.255.255.255", + ] + ) + func testCodableEncode(address: String) throws { + let original = try IPv4Address(address) + let encoded = try JSONEncoder().encode(original) + #expect(String(data: encoded, encoding: .utf8) == "\"\(address)\"") + } + + @Test( + "Codable decodes from string representation", + arguments: [ + "127.0.0.1", + "192.168.1.1", + "0.0.0.0", + "255.255.255.255", + ] + ) + func testCodableDecode(address: String) throws { + let json = Data("\"\(address)\"".utf8) + let decoded = try JSONDecoder().decode(IPv4Address.self, from: json) + let expected = try IPv4Address(address) + #expect(decoded == expected) + } } } diff --git a/Tests/ContainerizationExtrasTests/TestIPv6Address.swift b/Tests/ContainerizationExtrasTests/TestIPv6Address.swift index bd25371c..58eb3098 100644 --- a/Tests/ContainerizationExtrasTests/TestIPv6Address.swift +++ b/Tests/ContainerizationExtrasTests/TestIPv6Address.swift @@ -240,4 +240,35 @@ struct IPv6AddressTests { "Address \(addressString) (\(description)) should\(expected ? "" : " not") be documentation" ) } + + @Test( + "Codable encodes to string representation", + arguments: [ + ("::1", "::1"), + ("2001:db8::1", "2001:db8::1"), + ("::", "::"), + ("fe80::1", "fe80::1"), + ] + ) + func testCodableEncode(input: String, expected: String) throws { + let original = try IPv6Address(input) + let encoded = try JSONEncoder().encode(original) + #expect(String(data: encoded, encoding: .utf8) == "\"\(expected)\"") + } + + @Test( + "Codable decodes from string representation", + arguments: [ + "::1", + "2001:db8::1", + "::", + "fe80::1", + ] + ) + func testCodableDecode(address: String) throws { + let json = Data("\"\(address)\"".utf8) + let decoded = try JSONDecoder().decode(IPv6Address.self, from: json) + let expected = try IPv6Address(address) + #expect(decoded == expected) + } }