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) + } }