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.
This commit is contained in:
Yibo Zhuang
2025-12-20 17:11:36 -08:00
committed by GitHub
parent 528f635521
commit dcbc7bf71d
7 changed files with 181 additions and 4 deletions
+14 -1
View File
@@ -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)
}
}
+14 -1
View File
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
}
@@ -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)
}
}