Skip to content

Commit 70024e5

Browse files
committed
refactor: apply standard implementation patterns to RFC 1035
Refactors RFC 1035 to follow the standard patterns documented in STANDARD_IMPLEMENTATION_PATTERNS.md: **Type Refactoring:** - Domain.Label now conforms to UInt8.ASCII.Serializing - Domain now conforms to UInt8.ASCII.Serializing - Both types use canonical byte representation pattern - Added init(__unchecked:rawValue:) for internal use - Implemented case-insensitive Hashable per RFC 1035 **Protocol Conformances:** - RawRepresentable (automatic via Serializing) - CustomStringConvertible (automatic via Serializing) - Hashable with case-insensitive comparison - Added == operators for RawValue comparison **Error Handling:** - Added Sendable conformance to all Error types - Added byte parameter to Label.Error.invalidCharacters - Updated error descriptions with hex byte formatting **File Organization:** - Fixed file header: RFC_1035.swift (was File.swift) - Added exports.swift for INCITS_4_1986 re-export - Removed StringProtocol+RFC_1035.swift (auto-provided) - Removed [UInt8]+RFC_1035.swift (moved to type files) **Dependencies:** - Updated to INCITS_4_1986 0.3.0 for Serializing protocol This brings RFC 1035 into full compliance with the standard patterns used across all swift-standards implementations.
1 parent 741f0ae commit 70024e5

11 files changed

+473
-312
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ let package = Package(
2525
],
2626
dependencies: [
2727
.package(url: "https://github.com/swift-standards/swift-standards.git", from: "0.1.0"),
28-
.package(url: "https://github.com/swift-standards/swift-incits-4-1986.git", from: "0.1.0"),
28+
.package(url: "https://github.com/swift-standards/swift-incits-4-1986.git", from: "0.3.0"),
2929
],
3030
targets: [
3131
.target(

Sources/RFC 1035/RFC_1035.Domain.Error.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ extension RFC_1035.Domain {
2121
///
2222
/// These represent compositional constraint violations at the domain level,
2323
/// as defined by RFC 1035 Section 2.3.4.
24-
public enum Error: Swift.Error, Equatable {
24+
public enum Error: Swift.Error, Sendable, Equatable {
2525
/// Domain has no labels (empty string)
2626
case empty
2727

Sources/RFC 1035/RFC_1035.Domain.Label.Error.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ extension RFC_1035.Domain.Label {
2222
///
2323
/// These represent atomic constraint violations at the individual label level,
2424
/// as defined by RFC 1035 Section 2.3.1.
25-
public enum Error: Swift.Error, Equatable {
25+
public enum Error: Swift.Error, Sendable, Equatable {
2626
/// Label is empty
2727
case empty
2828

2929
/// Label exceeds maximum length of 63 octets
3030
case tooLong(_ length: Int, label: String)
3131

3232
/// Label contains invalid characters (must be letters, digits, or hyphens)
33-
case invalidCharacters(_ label: String)
33+
case invalidCharacters(_ label: String, byte: UInt8, reason: String)
3434

3535
/// Label starts with a hyphen (RFC 1035 violation)
3636
case startsWithHyphen(_ label: String)
@@ -52,8 +52,8 @@ extension RFC_1035.Domain.Label.Error: CustomStringConvertible {
5252
return "Domain label cannot be empty"
5353
case .tooLong(let length, let label):
5454
return "Domain label '\(label)' is too long (\(length) bytes, maximum 63)"
55-
case .invalidCharacters(let label):
56-
return "Domain label '\(label)' contains invalid characters (only letters, digits, and hyphens allowed)"
55+
case .invalidCharacters(let label, let byte, let reason):
56+
return "Domain label '\(label)' has invalid byte 0x\(String(byte, radix: 16)): \(reason)"
5757
case .startsWithHyphen(let label):
5858
return "Domain label '\(label)' cannot start with a hyphen"
5959
case .endsWithHyphen(let label):
Lines changed: 178 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,202 @@
11
//
2-
// File.swift
2+
// RFC_1035.Domain.Label.swift
33
// swift-rfc-1035
44
//
55
// Created by Coen ten Thije Boonkkamp on 20/11/2025.
66
//
77

8-
import Standards
8+
public import INCITS_4_1986
99

1010
extension RFC_1035.Domain {
11-
/// A type-safe domain label that enforces RFC 1035 rules
12-
public struct Label: Hashable, Sendable {
13-
/// Canonical byte storage (ASCII-only per RFC 1035)
14-
let _value: [UInt8]
11+
/// RFC 1035 compliant domain label
12+
///
13+
/// Represents a single label within a domain name as defined by RFC 1035 Section 2.3.1.
14+
/// Labels are case-insensitive ASCII strings with strict character restrictions.
15+
///
16+
/// ## RFC 1035 Constraints
17+
///
18+
/// Per RFC 1035 Section 2.3.1:
19+
/// - Must be 1-63 octets long
20+
/// - Must start with a letter (a-z, A-Z)
21+
/// - Must end with a letter or digit
22+
/// - May contain letters, digits, and hyphens in interior positions
23+
///
24+
/// ## Example
25+
///
26+
/// ```swift
27+
/// let label = try RFC_1035.Domain.Label("example")
28+
/// let invalid = try RFC_1035.Domain.Label("123") // Throws: must start with letter
29+
/// ```
30+
///
31+
/// ## RFC Reference
32+
///
33+
/// From RFC 1035 Section 2.3.1:
34+
///
35+
/// > labels must follow the rules for ARPANET host names. They must
36+
/// > start with a letter, end with a letter or digit, and have as interior
37+
/// > characters only letters, digits, and hyphen.
38+
public struct Label: Sendable, Codable {
39+
/// The label value
40+
public let rawValue: String
1541

16-
/// Initialize a label from a string, validating RFC 1035 rules
42+
/// Creates a label WITHOUT validation
1743
///
18-
/// This is the canonical initializer that performs validation.
19-
public init(_ string: some StringProtocol) throws(Error) {
20-
// Check emptiness
21-
guard !string.isEmpty else {
22-
throw Error.empty
23-
}
44+
/// **Warning**: Bypasses RFC 1035 validation.
45+
/// Only use with compile-time constants or pre-validated values.
46+
///
47+
/// - Parameters:
48+
/// - unchecked: Void parameter to prevent accidental use
49+
/// - rawValue: The raw label value (unchecked)
50+
init(
51+
__unchecked: Void,
52+
rawValue: String
53+
) {
54+
self.rawValue = rawValue
55+
}
56+
}
57+
}
2458

25-
// Check length
26-
guard string.count <= RFC_1035.Domain.Limits.maxLabelLength else {
27-
throw Error.tooLong(string.count, label: String(string))
59+
// MARK: - Hashable
60+
61+
extension RFC_1035.Domain.Label: Hashable {
62+
/// Hash value (case-insensitive per RFC 1035)
63+
public func hash(into hasher: inout Hasher) {
64+
hasher.combine(rawValue.lowercased())
65+
}
66+
67+
/// Equality comparison (case-insensitive per RFC 1035)
68+
public static func == (lhs: Self, rhs: Self) -> Bool {
69+
lhs.rawValue.lowercased() == rhs.rawValue.lowercased()
70+
}
71+
72+
/// Equality comparison with raw value (case-insensitive)
73+
public static func == (lhs: Self, rhs: Self.RawValue) -> Bool {
74+
lhs.rawValue.lowercased() == rhs.lowercased()
75+
}
76+
}
77+
78+
// MARK: - Serializing
79+
80+
extension RFC_1035.Domain.Label: UInt8.ASCII.Serializing {
81+
public static let serialize: @Sendable (Self) -> [UInt8] = [UInt8].init
82+
83+
/// Parses a domain label from canonical byte representation (CANONICAL PRIMITIVE)
84+
///
85+
/// This is the primitive parser that works at the byte level.
86+
/// RFC 1035 domain labels are ASCII-only.
87+
///
88+
/// ## RFC 1035 Compliance
89+
///
90+
/// Per RFC 1035 Section 2.3.1:
91+
/// - Labels must be 1-63 octets
92+
/// - Must start with a letter (a-z, A-Z)
93+
/// - Must end with a letter or digit
94+
/// - May contain letters, digits, and hyphens
95+
///
96+
/// ## Category Theory
97+
///
98+
/// This is the fundamental parsing transformation:
99+
/// - **Domain**: [UInt8] (ASCII bytes)
100+
/// - **Codomain**: RFC_1035.Domain.Label (structured data)
101+
///
102+
/// String-based parsing is derived as composition:
103+
/// ```
104+
/// String → [UInt8] (UTF-8 bytes) → Domain.Label
105+
/// ```
106+
///
107+
/// ## Example
108+
///
109+
/// ```swift
110+
/// let bytes = Array("example".utf8)
111+
/// let label = try RFC_1035.Domain.Label(ascii: bytes)
112+
/// ```
113+
///
114+
/// - Parameter bytes: The ASCII byte representation of the label
115+
/// - Throws: `RFC_1035.Domain.Label.Error` if the bytes are malformed
116+
public init(ascii bytes: [UInt8]) throws(Error) {
117+
// Empty check
118+
guard !bytes.isEmpty else {
119+
throw Error.empty
120+
}
121+
122+
// Length check (RFC 1035: max 63 octets)
123+
guard bytes.count <= RFC_1035.Domain.Limits.maxLabelLength else {
124+
let string = String(decoding: bytes, as: UTF8.self)
125+
throw Error.tooLong(bytes.count, label: string)
126+
}
127+
128+
let firstByte = bytes.first!
129+
let lastByte = bytes.last!
130+
131+
// Must start with a letter (RFC 1035)
132+
guard firstByte.ascii.isLetter else {
133+
let string = String(decoding: bytes, as: UTF8.self)
134+
if firstByte == .ascii.hyphen {
135+
throw Error.startsWithHyphen(string)
136+
} else if firstByte.ascii.isDigit {
137+
throw Error.startsWithDigit(string)
138+
} else {
139+
throw Error.invalidCharacters(string, byte: firstByte, reason: "Must start with a letter")
28140
}
141+
}
29142

30-
// Convert to String once for validation and error messages
31-
let stringValue = String(string)
32-
33-
// RFC 1035: Label must match pattern [a-zA-Z](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?
34-
guard (try? RFC_1035.Domain.labelRegex.wholeMatch(in: stringValue)) != nil else {
35-
// Provide more specific error
36-
if string.first == "-" {
37-
throw Error.startsWithHyphen(stringValue)
38-
} else if string.last == "-" {
39-
throw Error.endsWithHyphen(stringValue)
40-
} else if string.first?.isNumber == true {
41-
throw Error.startsWithDigit(stringValue)
42-
} else {
43-
throw Error.invalidCharacters(stringValue)
44-
}
143+
// Must end with a letter or digit (RFC 1035)
144+
guard lastByte.ascii.isLetter || lastByte.ascii.isDigit else {
145+
let string = String(decoding: bytes, as: UTF8.self)
146+
if lastByte == .ascii.hyphen {
147+
throw Error.endsWithHyphen(string)
148+
} else {
149+
throw Error.invalidCharacters(string, byte: lastByte, reason: "Must end with a letter or digit")
45150
}
151+
}
46152

47-
// Store as canonical byte representation (ASCII-only)
48-
self._value = [UInt8](utf8: string)
153+
// Interior characters: letters, digits, and hyphens only
154+
for byte in bytes {
155+
let valid = byte.ascii.isLetter || byte.ascii.isDigit || byte == .ascii.hyphen
156+
guard valid else {
157+
let string = String(decoding: bytes, as: UTF8.self)
158+
throw Error.invalidCharacters(string, byte: byte, reason: "Only letters, digits, and hyphens allowed")
159+
}
49160
}
50-
}
51-
}
52161

53-
extension RFC_1035.Domain.Label {
54-
/// String representation derived from canonical bytes
55-
public var value: String {
56-
String(self)
162+
self.init(__unchecked: (), rawValue: String(decoding: bytes, as: UTF8.self))
57163
}
58164
}
59165

60-
// MARK: - Convenience Initializers
61-
extension RFC_1035.Domain.Label {
62-
/// Initialize a label from bytes, validating RFC 1035 rules
166+
// MARK: - Byte Serialization
167+
168+
extension [UInt8] {
169+
/// Creates ASCII byte representation of an RFC 1035 domain label
170+
///
171+
/// This is the canonical serialization of domain labels to bytes.
172+
/// RFC 1035 domain labels are ASCII-only by definition.
173+
///
174+
/// ## Category Theory
63175
///
64-
/// Convenience initializer that decodes bytes as UTF-8 and validates.
65-
public init(_ bytes: [UInt8]) throws(Error) {
66-
// Decode bytes as UTF-8 and validate
67-
let string = String(decoding: bytes, as: UTF8.self)
68-
try self.init(string)
176+
/// This is the most universal serialization (natural transformation):
177+
/// - **Domain**: RFC_1035.Domain.Label (structured data)
178+
/// - **Codomain**: [UInt8] (ASCII bytes)
179+
///
180+
/// String representation is derived as composition:
181+
/// ```
182+
/// Domain.Label → [UInt8] (ASCII) → String (UTF-8 interpretation)
183+
/// ```
184+
///
185+
/// ## Example
186+
///
187+
/// ```swift
188+
/// let label = try RFC_1035.Domain.Label("example")
189+
/// let bytes = [UInt8](label)
190+
/// // bytes == "example" as ASCII bytes
191+
/// ```
192+
///
193+
/// - Parameter label: The domain label to serialize
194+
public init(_ label: RFC_1035.Domain.Label) {
195+
self = Array(label.rawValue.utf8)
69196
}
70197
}
71198

72-
extension RFC_1035.Domain.Label: CustomStringConvertible {
73-
public var description: String { String(self) }
74-
}
199+
// MARK: - Protocol Conformances
200+
201+
extension RFC_1035.Domain.Label: RawRepresentable {}
202+
extension RFC_1035.Domain.Label: CustomStringConvertible {}

0 commit comments

Comments
 (0)