hubcio commented on code in PR #4190: URL: https://github.com/apache/iggy/pull/4190#discussion_r4061008644
########## foreign/swift/Sources/Iggy/Wire/ByteCodec.swift: ########## @@ -0,0 +1,268 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import Foundation + +/// Failure while encoding or decoding wire bytes. Internal to the codec; the +/// client maps it onto an ``IggyError`` at the API boundary. +enum WireError: Error, Equatable { + case truncated(offset: Int, need: Int, have: Int) + case invalidUTF8(offset: Int) + case invalidDiscriminant(type: String, value: UInt8) + case validation(String) + case invalidBatchChecksum(stored: UInt64, computed: UInt64) + case invalidMessageChecksum(stored: UInt64, computed: UInt64, offset: UInt64) + case invalidMessageTimestampDelta(UInt64) + + var iggyError: IggyError { + switch self { + case .truncated(let offset, let need, let have): + IggyError(.invalidFormat, context: "unexpected end of buffer at offset \(offset): need \(need) bytes, have \(have)") + case .invalidUTF8(let offset): + IggyError(.invalidUtf8, context: "invalid utf-8 at offset \(offset)") + case .invalidDiscriminant(let type, let value): + IggyError(.invalidFormat, context: "unknown discriminant \(value) for \(type)") + case .validation(let message): + IggyError(.invalidFormat, context: message) + case .invalidBatchChecksum(let stored, let computed): + IggyError(.invalidBatchChecksum, context: "stored \(stored), computed \(computed)") + case .invalidMessageChecksum(let stored, let computed, let offset): + IggyError(.invalidMessageChecksum, context: "stored \(stored), computed \(computed), offset \(offset)") + case .invalidMessageTimestampDelta(let delta): + IggyError(.invalidMessageTimestampDelta, context: "delta \(delta) microseconds") + } + } +} + +/// Little-endian append-only encoder over a byte array. +struct ByteWriter { + private(set) var bytes: [UInt8] + + init(capacity: Int = 0) { + bytes = [] + bytes.reserveCapacity(capacity) + } + + var count: Int { bytes.count } + + mutating func write(_ value: UInt8) { + bytes.append(value) + } + + mutating func write(_ value: Bool) { + bytes.append(value ? 1 : 0) + } + + mutating func write(_ value: UInt16) { + withUnsafeBytes(of: value.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func write(_ value: UInt32) { + withUnsafeBytes(of: value.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func write(_ value: UInt64) { + withUnsafeBytes(of: value.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func write(_ value: Float) { + write(value.bitPattern) + } + + mutating func write(_ value: UInt128Value) { + write(value.low) + write(value.high) + } + + mutating func write(_ value: [UInt8]) { + bytes.append(contentsOf: value) + } + + mutating func write(_ value: ArraySlice<UInt8>) { + bytes.append(contentsOf: value) + } + + mutating func write(_ value: String) { + bytes.append(contentsOf: value.utf8) + } + + /// `[len: u8][utf8]`, the layout of every wire name. The caller guarantees + /// the length fits. + mutating func writeName(_ value: String) { + let utf8 = Array(value.utf8) + bytes.append(UInt8(utf8.count)) Review Comment: warning: `writeName` traps when a name is longer than 255 bytes, because `UInt8(utf8.count)` cannot hold the count. make it throw `WireError`, like `readName` already does, and reject an empty name too. ########## foreign/swift/Sources/Iggy/Wire/ByteCodec.swift: ########## @@ -0,0 +1,268 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import Foundation + +/// Failure while encoding or decoding wire bytes. Internal to the codec; the +/// client maps it onto an ``IggyError`` at the API boundary. +enum WireError: Error, Equatable { + case truncated(offset: Int, need: Int, have: Int) + case invalidUTF8(offset: Int) + case invalidDiscriminant(type: String, value: UInt8) + case validation(String) + case invalidBatchChecksum(stored: UInt64, computed: UInt64) + case invalidMessageChecksum(stored: UInt64, computed: UInt64, offset: UInt64) + case invalidMessageTimestampDelta(UInt64) + + var iggyError: IggyError { + switch self { + case .truncated(let offset, let need, let have): + IggyError(.invalidFormat, context: "unexpected end of buffer at offset \(offset): need \(need) bytes, have \(have)") + case .invalidUTF8(let offset): + IggyError(.invalidUtf8, context: "invalid utf-8 at offset \(offset)") + case .invalidDiscriminant(let type, let value): + IggyError(.invalidFormat, context: "unknown discriminant \(value) for \(type)") + case .validation(let message): + IggyError(.invalidFormat, context: message) + case .invalidBatchChecksum(let stored, let computed): + IggyError(.invalidBatchChecksum, context: "stored \(stored), computed \(computed)") + case .invalidMessageChecksum(let stored, let computed, let offset): + IggyError(.invalidMessageChecksum, context: "stored \(stored), computed \(computed), offset \(offset)") + case .invalidMessageTimestampDelta(let delta): + IggyError(.invalidMessageTimestampDelta, context: "delta \(delta) microseconds") + } + } +} + +/// Little-endian append-only encoder over a byte array. +struct ByteWriter { + private(set) var bytes: [UInt8] + + init(capacity: Int = 0) { + bytes = [] + bytes.reserveCapacity(capacity) + } + + var count: Int { bytes.count } + + mutating func write(_ value: UInt8) { + bytes.append(value) + } + + mutating func write(_ value: Bool) { + bytes.append(value ? 1 : 0) + } + + mutating func write(_ value: UInt16) { + withUnsafeBytes(of: value.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func write(_ value: UInt32) { + withUnsafeBytes(of: value.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func write(_ value: UInt64) { + withUnsafeBytes(of: value.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func write(_ value: Float) { + write(value.bitPattern) + } + + mutating func write(_ value: UInt128Value) { + write(value.low) + write(value.high) + } + + mutating func write(_ value: [UInt8]) { + bytes.append(contentsOf: value) + } + + mutating func write(_ value: ArraySlice<UInt8>) { + bytes.append(contentsOf: value) + } + + mutating func write(_ value: String) { + bytes.append(contentsOf: value.utf8) + } + + /// `[len: u8][utf8]`, the layout of every wire name. The caller guarantees + /// the length fits. + mutating func writeName(_ value: String) { + let utf8 = Array(value.utf8) + bytes.append(UInt8(utf8.count)) + bytes.append(contentsOf: utf8) + } + + /// `[len: u32][utf8]`, the layout of longer free-form strings. + mutating func writeLongString(_ value: String) { + let utf8 = Array(value.utf8) + write(UInt32(utf8.count)) + bytes.append(contentsOf: utf8) + } + + mutating func writeZeros(_ count: Int) { + bytes.append(contentsOf: repeatElement(0, count: count)) + } + + /// Overwrites `count` bytes at `offset` with `value`; `value` must have + /// exactly `count` bytes and the range must already exist. + mutating func overwrite(at offset: Int, with value: [UInt8]) { + bytes.replaceSubrange(offset..<offset + value.count, with: value) + } +} + +/// Little-endian cursor over a byte slice. Every read is bounds-checked and +/// fails with ``WireError/truncated(offset:need:have:)`` instead of trapping, +/// because the bytes come from the network. +struct ByteReader { + let bytes: ArraySlice<UInt8> + private(set) var offset: Int + + init(_ bytes: ArraySlice<UInt8>) { + self.bytes = bytes + self.offset = bytes.startIndex + } + + init(_ bytes: [UInt8]) { + self.init(bytes[...]) + } + + var isAtEnd: Bool { offset >= bytes.endIndex } + var remaining: Int { bytes.endIndex - offset } + /// Position relative to the start of the buffer handed to the reader. + var position: Int { offset - bytes.startIndex } + + private func require(_ count: Int) throws { + if remaining < count { + throw WireError.truncated(offset: position, need: count, have: remaining) + } + } + + mutating func readUInt8() throws -> UInt8 { + try require(1) + let value = bytes[offset] + offset += 1 + return value + } + + mutating func readBool() throws -> Bool { + try readUInt8() != 0 + } + + mutating func readUInt16() throws -> UInt16 { + try require(2) + let value = bytes.withUnsafeBytes { $0.loadUnaligned(fromByteOffset: position, as: UInt16.self) } + offset += 2 + return UInt16(littleEndian: value) + } + + mutating func readUInt32() throws -> UInt32 { + try require(4) + let value = bytes.withUnsafeBytes { $0.loadUnaligned(fromByteOffset: position, as: UInt32.self) } + offset += 4 + return UInt32(littleEndian: value) + } + + mutating func readUInt64() throws -> UInt64 { + try require(8) + let value = bytes.withUnsafeBytes { $0.loadUnaligned(fromByteOffset: position, as: UInt64.self) } + offset += 8 + return UInt64(littleEndian: value) + } + + mutating func readFloat() throws -> Float { + Float(bitPattern: try readUInt32()) + } + + mutating func readUInt128() throws -> UInt128Value { + let low = try readUInt64() + let high = try readUInt64() + return UInt128Value(low: low, high: high) + } + + mutating func readBytes(_ count: Int) throws -> ArraySlice<UInt8> { + try require(count) + let slice = bytes[offset..<offset + count] + offset += count + return slice + } + + mutating func readString(_ count: Int) throws -> String { + let start = position + let slice = try readBytes(count) + guard let value = String(bytes: slice, encoding: .utf8) else { + throw WireError.invalidUTF8(offset: start) + } + return value + } + + /// `[len: u8][utf8]` with the 1...255 byte bound every wire name carries. + mutating func readName() throws -> String { + let length = Int(try readUInt8()) + if length == 0 { + throw WireError.validation("wire name must be 1-255 bytes, got 0") + } + return try readString(length) + } + + /// `[len: u32][utf8]`. + mutating func readLongString() throws -> String { + let length = Int(try readUInt32()) Review Comment: warning: this traps on a 32-bit `Int` platform when the length is above `Int32.max`, and `Package.swift` declares watchOS. use `Int(exactly:)` and throw `WireError.truncated` on nil. ########## foreign/swift/Sources/Iggy/Utilities/UInt128Value.swift: ########## @@ -0,0 +1,155 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import Foundation + +/// An unsigned 128-bit integer split into two 64-bit halves. +/// +/// Message identifiers and 128-bit user headers travel as `u128` on the wire. +/// The standard library's `UInt128` is only available from macOS 15 / iOS 18, +/// so the SDK carries its own representation that works on every supported +/// platform. `low` holds the least significant 64 bits. +public struct UInt128Value: Sendable, Hashable, Codable { + public var low: UInt64 + public var high: UInt64 + + public init(low: UInt64, high: UInt64) { + self.low = low + self.high = high + } + + public init(_ value: UInt64) { + self.init(low: value, high: 0) + } + + /// The 16 bytes of a UUID interpreted as a little-endian integer, which is + /// how the Rust SDK derives message identifiers from `Uuid::to_u128_le`. + public init(uuid: UUID) { + let bytes = withUnsafeBytes(of: uuid.uuid) { Array($0) } + low = UInt64(littleEndianBytes: bytes[0..<8]) + high = UInt64(littleEndianBytes: bytes[8..<16]) + } + + public static let zero = UInt128Value(low: 0, high: 0) + + /// A random identifier, never zero. + public static func random() -> UInt128Value { + var value = UInt128Value(uuid: UUID()) + if value == .zero { + value.low = 1 + } + return value + } + + public var isZero: Bool { low == 0 && high == 0 } + + /// Little-endian wire bytes. + public var littleEndianBytes: [UInt8] { + low.littleEndianBytes + high.littleEndianBytes + } + + public init(littleEndianBytes bytes: ArraySlice<UInt8>) { + precondition(bytes.count == 16, "a 128-bit value needs exactly 16 bytes") + let start = bytes.startIndex + low = UInt64(littleEndianBytes: bytes[start..<start + 8]) + high = UInt64(littleEndianBytes: bytes[start + 8..<start + 16]) + } + + /// The value reinterpreted as a UUID (bytes in little-endian order). + public var uuid: UUID { Review Comment: warning: `uuid` allocates three arrays to move 16 bytes, and `random()` allocates one per identifier. read and write both halves with `withUnsafeBytes` instead - the Rust SDK keeps a message id on the stack. ########## foreign/swift/Package.swift: ########## @@ -0,0 +1,55 @@ +// swift-tools-version: 6.0 +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import PackageDescription + +let package = Package( + name: "iggy", + platforms: [ Review Comment: warning: the manifest claims five Apple platforms, but every Swift job runs on `ubuntu-latest`, so none of them is ever compiled. add a macOS lane, or drop the list. ########## .github/config/components.yml: ########## @@ -335,6 +335,18 @@ components: - "foreign/cpp/**" tasks: ["lint", "build", "test"] + sdk-swift: + depends_on: + - "rust-sdk" # Swift SDK depends on core SDK + - "rust-server" # For integration tests + - "ci-infrastructure" # CI changes trigger full regression + paths: + - "foreign/swift/**" Review Comment: nit: `sdk-java` lists its example tree here, but `sdk-swift` does not, and `.gitignore` already reserves `examples/swift/**`. add the path, or an example change will not trigger this lane. ########## .github/actions/swift/pre-merge/action.yml: ########## @@ -0,0 +1,66 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: swift-pre-merge +description: Swift pre-merge testing github iggy actions + +inputs: + task: + description: "Task to run (lint, test, build)" + required: true + +runs: + using: "composite" + steps: + - name: Setup Swift + uses: ./.github/actions/utils/setup-swift + with: + cache-key: ${{ inputs.task }} + + # swift-format ships with the toolchain; the configuration lives in + # foreign/swift/.swift-format and the examples and BDD packages reuse it. + - name: Format Check + shell: bash + if: inputs.task == 'lint' + run: | + cd foreign/swift + swift format lint --strict --recursive Sources Tests Package.swift + + # A warning-free build is the lint gate the Swift compiler provides: the + # SDK builds in Swift 6 language mode with strict concurrency checking. + - name: Build with warnings as errors + shell: bash + if: inputs.task == 'lint' + run: | + cd foreign/swift + swift build -Xswiftc -warnings-as-errors + + - name: Test + shell: bash + if: inputs.task == 'test' + run: | + cd foreign/swift + # The end-to-end suite skips itself while IGGY_TCP_ADDRESS is unset, Review Comment: nit: this describes an end-to-end suite that skips itself while `IGGY_TCP_ADDRESS` is unset, and no such suite exists. drop the comment until it lands. ########## .github/actions/utils/setup-swift/action.yml: ########## @@ -0,0 +1,52 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: setup-swift +description: Install the Swift toolchain and cache SwiftPM build products +inputs: + swift-version: + description: "Swift toolchain version to install" + required: false + default: "6.1" + cache-key: + description: "Extra cache key segment, one per package that builds in the job" + required: false + default: "sdk" + +runs: + using: "composite" + steps: + - name: Install Swift + uses: swift-actions/setup-swift@v2 + with: + swift-version: ${{ inputs.swift-version }} + + - name: Print Swift version + shell: bash + run: swift --version + + # Package.resolved pins the dependency graph, so the resolved checkouts + # and the compiled dependencies are a stable cache key. + - name: Cache SwiftPM dependencies + uses: actions/[email protected] + with: + path: | Review Comment: warning: the cache lists only the checkouts, so all three jobs rebuild BoringSSL from source. add `.build/debug` and `.build/release`, and fix the description on line 19 that promises cached build products. ########## foreign/swift/Package.swift: ########## @@ -0,0 +1,55 @@ +// swift-tools-version: 6.0 +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import PackageDescription + +let package = Package( + name: "iggy", + platforms: [ + .macOS(.v13), + .iOS(.v16), + .tvOS(.v16), + .watchOS(.v9), + .visionOS(.v1), + ], + products: [ + .library(name: "Iggy", targets: ["Iggy"]) + ], + dependencies: [ Review Comment: warning: nothing in the package imports swift-nio, swift-nio-ssl or swift-log, so every lane compiles BoringSSL three times for no reason. drop the dependencies until the transport layer needs them. ########## .github/actions/swift/pre-merge/action.yml: ########## @@ -0,0 +1,66 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: swift-pre-merge +description: Swift pre-merge testing github iggy actions + +inputs: + task: + description: "Task to run (lint, test, build)" + required: true + +runs: + using: "composite" + steps: + - name: Setup Swift + uses: ./.github/actions/utils/setup-swift + with: + cache-key: ${{ inputs.task }} Review Comment: warning: the key carries the task name, so lint, test and build each fetch and store their own copy of the dependency graph. use one constant key per package. ########## foreign/swift/Sources/Iggy/Wire/ByteCodec.swift: ########## @@ -0,0 +1,268 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import Foundation + +/// Failure while encoding or decoding wire bytes. Internal to the codec; the +/// client maps it onto an ``IggyError`` at the API boundary. +enum WireError: Error, Equatable { + case truncated(offset: Int, need: Int, have: Int) + case invalidUTF8(offset: Int) + case invalidDiscriminant(type: String, value: UInt8) + case validation(String) + case invalidBatchChecksum(stored: UInt64, computed: UInt64) + case invalidMessageChecksum(stored: UInt64, computed: UInt64, offset: UInt64) + case invalidMessageTimestampDelta(UInt64) + + var iggyError: IggyError { + switch self { + case .truncated(let offset, let need, let have): + IggyError(.invalidFormat, context: "unexpected end of buffer at offset \(offset): need \(need) bytes, have \(have)") + case .invalidUTF8(let offset): + IggyError(.invalidUtf8, context: "invalid utf-8 at offset \(offset)") + case .invalidDiscriminant(let type, let value): + IggyError(.invalidFormat, context: "unknown discriminant \(value) for \(type)") + case .validation(let message): + IggyError(.invalidFormat, context: message) + case .invalidBatchChecksum(let stored, let computed): + IggyError(.invalidBatchChecksum, context: "stored \(stored), computed \(computed)") + case .invalidMessageChecksum(let stored, let computed, let offset): + IggyError(.invalidMessageChecksum, context: "stored \(stored), computed \(computed), offset \(offset)") + case .invalidMessageTimestampDelta(let delta): + IggyError(.invalidMessageTimestampDelta, context: "delta \(delta) microseconds") + } + } +} + +/// Little-endian append-only encoder over a byte array. +struct ByteWriter { + private(set) var bytes: [UInt8] + + init(capacity: Int = 0) { + bytes = [] + bytes.reserveCapacity(capacity) + } + + var count: Int { bytes.count } + + mutating func write(_ value: UInt8) { + bytes.append(value) + } + + mutating func write(_ value: Bool) { + bytes.append(value ? 1 : 0) + } + + mutating func write(_ value: UInt16) { + withUnsafeBytes(of: value.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func write(_ value: UInt32) { + withUnsafeBytes(of: value.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func write(_ value: UInt64) { + withUnsafeBytes(of: value.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func write(_ value: Float) { + write(value.bitPattern) + } + + mutating func write(_ value: UInt128Value) { + write(value.low) + write(value.high) + } + + mutating func write(_ value: [UInt8]) { + bytes.append(contentsOf: value) + } + + mutating func write(_ value: ArraySlice<UInt8>) { + bytes.append(contentsOf: value) + } + + mutating func write(_ value: String) { + bytes.append(contentsOf: value.utf8) + } + + /// `[len: u8][utf8]`, the layout of every wire name. The caller guarantees + /// the length fits. + mutating func writeName(_ value: String) { + let utf8 = Array(value.utf8) Review Comment: warning: this copies the UTF-8 view into a fresh array for every name written, and line 113 does the same. append the view directly and read its count instead. ########## foreign/swift/Sources/Iggy/Errors/IggyErrorCode.swift: ########## @@ -0,0 +1,514 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// Generated from `core/common/src/error/iggy_error.rs` by Review Comment: warning: the comment names `Tools/golden-vectors`, which does not exist in this PR, and nothing pins the 240-entry table. name the real generator and add a table test, like the Java SDK's `IggyErrorCodeTest`. ########## .github/config/components.yml: ########## @@ -443,6 +455,7 @@ components: - "bdd/scenarios/**" tasks: ["bdd-cpp"] + Review Comment: nit: this blank line is unrelated to the Swift component - a blank line already sits above it. drop it. also at line 542. ########## foreign/swift/Sources/Iggy/Wire/ByteCodec.swift: ########## @@ -0,0 +1,268 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import Foundation + +/// Failure while encoding or decoding wire bytes. Internal to the codec; the +/// client maps it onto an ``IggyError`` at the API boundary. +enum WireError: Error, Equatable { + case truncated(offset: Int, need: Int, have: Int) + case invalidUTF8(offset: Int) + case invalidDiscriminant(type: String, value: UInt8) + case validation(String) + case invalidBatchChecksum(stored: UInt64, computed: UInt64) + case invalidMessageChecksum(stored: UInt64, computed: UInt64, offset: UInt64) + case invalidMessageTimestampDelta(UInt64) + + var iggyError: IggyError { + switch self { + case .truncated(let offset, let need, let have): + IggyError(.invalidFormat, context: "unexpected end of buffer at offset \(offset): need \(need) bytes, have \(have)") + case .invalidUTF8(let offset): + IggyError(.invalidUtf8, context: "invalid utf-8 at offset \(offset)") + case .invalidDiscriminant(let type, let value): + IggyError(.invalidFormat, context: "unknown discriminant \(value) for \(type)") + case .validation(let message): + IggyError(.invalidFormat, context: message) + case .invalidBatchChecksum(let stored, let computed): + IggyError(.invalidBatchChecksum, context: "stored \(stored), computed \(computed)") + case .invalidMessageChecksum(let stored, let computed, let offset): + IggyError(.invalidMessageChecksum, context: "stored \(stored), computed \(computed), offset \(offset)") + case .invalidMessageTimestampDelta(let delta): + IggyError(.invalidMessageTimestampDelta, context: "delta \(delta) microseconds") + } + } +} + +/// Little-endian append-only encoder over a byte array. +struct ByteWriter { + private(set) var bytes: [UInt8] + + init(capacity: Int = 0) { + bytes = [] + bytes.reserveCapacity(capacity) + } + + var count: Int { bytes.count } + + mutating func write(_ value: UInt8) { + bytes.append(value) + } + + mutating func write(_ value: Bool) { + bytes.append(value ? 1 : 0) + } + + mutating func write(_ value: UInt16) { + withUnsafeBytes(of: value.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func write(_ value: UInt32) { + withUnsafeBytes(of: value.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func write(_ value: UInt64) { + withUnsafeBytes(of: value.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func write(_ value: Float) { + write(value.bitPattern) + } + + mutating func write(_ value: UInt128Value) { + write(value.low) + write(value.high) + } + + mutating func write(_ value: [UInt8]) { + bytes.append(contentsOf: value) + } + + mutating func write(_ value: ArraySlice<UInt8>) { + bytes.append(contentsOf: value) + } + + mutating func write(_ value: String) { + bytes.append(contentsOf: value.utf8) + } + + /// `[len: u8][utf8]`, the layout of every wire name. The caller guarantees + /// the length fits. + mutating func writeName(_ value: String) { + let utf8 = Array(value.utf8) + bytes.append(UInt8(utf8.count)) + bytes.append(contentsOf: utf8) + } + + /// `[len: u32][utf8]`, the layout of longer free-form strings. + mutating func writeLongString(_ value: String) { + let utf8 = Array(value.utf8) + write(UInt32(utf8.count)) + bytes.append(contentsOf: utf8) + } + + mutating func writeZeros(_ count: Int) { + bytes.append(contentsOf: repeatElement(0, count: count)) + } + + /// Overwrites `count` bytes at `offset` with `value`; `value` must have Review Comment: nit: the doc names a `count` parameter that the signature does not have, and it does not say that a range past the end of the buffer traps. describe `value.count` and the trap. ########## foreign/swift/Sources/Iggy/Utilities/UInt128Value.swift: ########## @@ -0,0 +1,155 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import Foundation + +/// An unsigned 128-bit integer split into two 64-bit halves. +/// +/// Message identifiers and 128-bit user headers travel as `u128` on the wire. +/// The standard library's `UInt128` is only available from macOS 15 / iOS 18, +/// so the SDK carries its own representation that works on every supported +/// platform. `low` holds the least significant 64 bits. +public struct UInt128Value: Sendable, Hashable, Codable { + public var low: UInt64 + public var high: UInt64 + + public init(low: UInt64, high: UInt64) { + self.low = low + self.high = high + } + + public init(_ value: UInt64) { + self.init(low: value, high: 0) + } + + /// The 16 bytes of a UUID interpreted as a little-endian integer, which is + /// how the Rust SDK derives message identifiers from `Uuid::to_u128_le`. + public init(uuid: UUID) { + let bytes = withUnsafeBytes(of: uuid.uuid) { Array($0) } + low = UInt64(littleEndianBytes: bytes[0..<8]) + high = UInt64(littleEndianBytes: bytes[8..<16]) + } + + public static let zero = UInt128Value(low: 0, high: 0) + + /// A random identifier, never zero. + public static func random() -> UInt128Value { + var value = UInt128Value(uuid: UUID()) + if value == .zero { + value.low = 1 + } + return value + } + + public var isZero: Bool { low == 0 && high == 0 } + + /// Little-endian wire bytes. + public var littleEndianBytes: [UInt8] { + low.littleEndianBytes + high.littleEndianBytes + } + + public init(littleEndianBytes bytes: ArraySlice<UInt8>) { + precondition(bytes.count == 16, "a 128-bit value needs exactly 16 bytes") Review Comment: nit: a slice that is not 16 bytes kills the process instead of returning an error. state the requirement in the doc comment, or make the initializer failable. ########## foreign/swift/Sources/Iggy/Wire/ByteCodec.swift: ########## @@ -0,0 +1,268 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import Foundation + +/// Failure while encoding or decoding wire bytes. Internal to the codec; the +/// client maps it onto an ``IggyError`` at the API boundary. +enum WireError: Error, Equatable { + case truncated(offset: Int, need: Int, have: Int) + case invalidUTF8(offset: Int) + case invalidDiscriminant(type: String, value: UInt8) + case validation(String) + case invalidBatchChecksum(stored: UInt64, computed: UInt64) + case invalidMessageChecksum(stored: UInt64, computed: UInt64, offset: UInt64) + case invalidMessageTimestampDelta(UInt64) + + var iggyError: IggyError { + switch self { + case .truncated(let offset, let need, let have): + IggyError(.invalidFormat, context: "unexpected end of buffer at offset \(offset): need \(need) bytes, have \(have)") + case .invalidUTF8(let offset): + IggyError(.invalidUtf8, context: "invalid utf-8 at offset \(offset)") + case .invalidDiscriminant(let type, let value): + IggyError(.invalidFormat, context: "unknown discriminant \(value) for \(type)") + case .validation(let message): + IggyError(.invalidFormat, context: message) + case .invalidBatchChecksum(let stored, let computed): + IggyError(.invalidBatchChecksum, context: "stored \(stored), computed \(computed)") + case .invalidMessageChecksum(let stored, let computed, let offset): + IggyError(.invalidMessageChecksum, context: "stored \(stored), computed \(computed), offset \(offset)") + case .invalidMessageTimestampDelta(let delta): + IggyError(.invalidMessageTimestampDelta, context: "delta \(delta) microseconds") + } + } +} + +/// Little-endian append-only encoder over a byte array. +struct ByteWriter { + private(set) var bytes: [UInt8] + + init(capacity: Int = 0) { Review Comment: warning: the writer starts at capacity 0 and doubles, and nothing computes an encoded size to pass to `init(capacity:)`. add `encodedSize`, the way `core/binary_protocol/src/codec.rs` does. ########## foreign/swift/Sources/Iggy/Iggy.swift: ########## @@ -0,0 +1,24 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +/// Version identity the SDK sends to the server during the login handshake. +public enum IggyVersion { + /// Name of this SDK as reported to the server. + public static let sdkName = "swift-sdk" + /// Version of this SDK as reported to the server. + public static let sdkVersion = "0.1.0" Review Comment: nit: `sdkVersion` is hardcoded to 0.1.0 and no tooling maintains it, unlike every other foreign SDK. add it to the release machinery, or mark it provisional. ########## .github/actions/swift/pre-merge/action.yml: ########## @@ -0,0 +1,66 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: swift-pre-merge +description: Swift pre-merge testing github iggy actions + +inputs: + task: + description: "Task to run (lint, test, build)" + required: true + +runs: + using: "composite" + steps: + - name: Setup Swift + uses: ./.github/actions/utils/setup-swift + with: + cache-key: ${{ inputs.task }} + + # swift-format ships with the toolchain; the configuration lives in Review Comment: nit: this says the examples and BDD packages reuse this `.swift-format`, and neither directory exists. state only where the formatter file lives. ########## foreign/swift/Package.swift: ########## @@ -0,0 +1,55 @@ +// swift-tools-version: 6.0 +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import PackageDescription + +let package = Package( + name: "iggy", Review Comment: nit: the package is named `iggy` while the Node and Python distributions are `apache-iggy`, and consumers write this name in `.product(name:package:)`. rename it, unless the short name is deliberate. ########## foreign/swift/Sources/Iggy/Errors/IggyError.swift: ########## @@ -0,0 +1,103 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +/// Error raised by every SDK operation. +/// +/// The `code` is what the server answered with, or what the SDK decided +/// locally (a dropped connection, a rejected argument). Match on it: +/// +/// ```swift +/// do { +/// _ = try await client.getStream("orders") +/// } catch let error as IggyError where error.code == .streamNameNotFound { +/// // create it +/// } +/// ``` +/// +/// `context` carries whatever extra detail was available where the error was +/// raised, such as the transport failure message or the protocol window the +/// server accepts. It never carries credentials. +public struct IggyError: Error, Sendable, Hashable, CustomStringConvertible { + /// The classified code. + public let code: IggyErrorCode + /// The exact code that arrived on the wire. Equal to `code.rawValue` unless + /// the server answered with a code this SDK does not know, in which case + /// `code` is ``IggyErrorCode/error`` and this keeps the original value. + public let rawCode: UInt32 + /// Extra detail about the failure, when any was available. + public let context: String? + + public init(_ code: IggyErrorCode, context: String? = nil) { + self.code = code + self.rawCode = code.rawValue + self.context = context + } + + /// Maps a code received from the server, keeping unknown codes visible. + public init(wireCode: UInt32) { + if let code = IggyErrorCode(rawValue: wireCode) { + self.code = code + self.context = nil + } else { + self.code = .error + self.context = "unknown error code \(wireCode)" + } + self.rawCode = wireCode + } + + public var description: String { + if let context { + return "\(code.name) (\(rawCode)): \(context)" + } + return "\(code.name) (\(rawCode))" + } +} + +extension IggyError { Review Comment: simplification: these three predicates have no caller, and two differ only by `.unauthenticated`. delete the extension until the reconnect and credential paths exist, or write `isConnectionLoss || code == .unauthenticated`. ########## foreign/swift/Sources/Iggy/Wire/ByteCodec.swift: ########## @@ -0,0 +1,268 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import Foundation + +/// Failure while encoding or decoding wire bytes. Internal to the codec; the +/// client maps it onto an ``IggyError`` at the API boundary. +enum WireError: Error, Equatable { + case truncated(offset: Int, need: Int, have: Int) + case invalidUTF8(offset: Int) + case invalidDiscriminant(type: String, value: UInt8) + case validation(String) + case invalidBatchChecksum(stored: UInt64, computed: UInt64) + case invalidMessageChecksum(stored: UInt64, computed: UInt64, offset: UInt64) + case invalidMessageTimestampDelta(UInt64) + + var iggyError: IggyError { + switch self { + case .truncated(let offset, let need, let have): + IggyError(.invalidFormat, context: "unexpected end of buffer at offset \(offset): need \(need) bytes, have \(have)") + case .invalidUTF8(let offset): + IggyError(.invalidUtf8, context: "invalid utf-8 at offset \(offset)") + case .invalidDiscriminant(let type, let value): + IggyError(.invalidFormat, context: "unknown discriminant \(value) for \(type)") + case .validation(let message): + IggyError(.invalidFormat, context: message) + case .invalidBatchChecksum(let stored, let computed): + IggyError(.invalidBatchChecksum, context: "stored \(stored), computed \(computed)") + case .invalidMessageChecksum(let stored, let computed, let offset): + IggyError(.invalidMessageChecksum, context: "stored \(stored), computed \(computed), offset \(offset)") + case .invalidMessageTimestampDelta(let delta): + IggyError(.invalidMessageTimestampDelta, context: "delta \(delta) microseconds") + } + } +} + +/// Little-endian append-only encoder over a byte array. +struct ByteWriter { + private(set) var bytes: [UInt8] + + init(capacity: Int = 0) { + bytes = [] + bytes.reserveCapacity(capacity) + } + + var count: Int { bytes.count } + + mutating func write(_ value: UInt8) { + bytes.append(value) + } + + mutating func write(_ value: Bool) { + bytes.append(value ? 1 : 0) + } + + mutating func write(_ value: UInt16) { + withUnsafeBytes(of: value.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func write(_ value: UInt32) { + withUnsafeBytes(of: value.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func write(_ value: UInt64) { + withUnsafeBytes(of: value.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func write(_ value: Float) { + write(value.bitPattern) + } + + mutating func write(_ value: UInt128Value) { + write(value.low) + write(value.high) + } + + mutating func write(_ value: [UInt8]) { + bytes.append(contentsOf: value) + } + + mutating func write(_ value: ArraySlice<UInt8>) { + bytes.append(contentsOf: value) + } + + mutating func write(_ value: String) { + bytes.append(contentsOf: value.utf8) + } + + /// `[len: u8][utf8]`, the layout of every wire name. The caller guarantees + /// the length fits. + mutating func writeName(_ value: String) { + let utf8 = Array(value.utf8) + bytes.append(UInt8(utf8.count)) + bytes.append(contentsOf: utf8) + } + + /// `[len: u32][utf8]`, the layout of longer free-form strings. + mutating func writeLongString(_ value: String) { + let utf8 = Array(value.utf8) + write(UInt32(utf8.count)) + bytes.append(contentsOf: utf8) + } + + mutating func writeZeros(_ count: Int) { + bytes.append(contentsOf: repeatElement(0, count: count)) + } + + /// Overwrites `count` bytes at `offset` with `value`; `value` must have + /// exactly `count` bytes and the range must already exist. + mutating func overwrite(at offset: Int, with value: [UInt8]) { + bytes.replaceSubrange(offset..<offset + value.count, with: value) + } +} + +/// Little-endian cursor over a byte slice. Every read is bounds-checked and +/// fails with ``WireError/truncated(offset:need:have:)`` instead of trapping, +/// because the bytes come from the network. +struct ByteReader { + let bytes: ArraySlice<UInt8> + private(set) var offset: Int + + init(_ bytes: ArraySlice<UInt8>) { + self.bytes = bytes + self.offset = bytes.startIndex + } + + init(_ bytes: [UInt8]) { + self.init(bytes[...]) + } + + var isAtEnd: Bool { offset >= bytes.endIndex } + var remaining: Int { bytes.endIndex - offset } + /// Position relative to the start of the buffer handed to the reader. + var position: Int { offset - bytes.startIndex } + + private func require(_ count: Int) throws { + if remaining < count { + throw WireError.truncated(offset: position, need: count, have: remaining) + } + } + + mutating func readUInt8() throws -> UInt8 { + try require(1) + let value = bytes[offset] + offset += 1 + return value + } + + mutating func readBool() throws -> Bool { + try readUInt8() != 0 + } + + mutating func readUInt16() throws -> UInt16 { Review Comment: simplification: the readers inline their own unaligned loads while four of the six `littleEndianBytes` helpers go unused. read the bytes and delegate to one helper per width, then delete the rest. ########## foreign/swift/Sources/Iggy/Errors/IggyError.swift: ########## @@ -0,0 +1,103 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +/// Error raised by every SDK operation. +/// +/// The `code` is what the server answered with, or what the SDK decided +/// locally (a dropped connection, a rejected argument). Match on it: +/// +/// ```swift +/// do { +/// _ = try await client.getStream("orders") +/// } catch let error as IggyError where error.code == .streamNameNotFound { +/// // create it +/// } +/// ``` +/// +/// `context` carries whatever extra detail was available where the error was +/// raised, such as the transport failure message or the protocol window the +/// server accepts. It never carries credentials. +public struct IggyError: Error, Sendable, Hashable, CustomStringConvertible { Review Comment: nit: synthesized equality compares `context` too, so `IggyError(.invalidFormat, context: "x")` is unequal here and equal in the Rust SDK. compare `code` alone, or note the difference in the doc. ########## foreign/swift/Sources/Iggy/Wire/ByteCodec.swift: ########## @@ -0,0 +1,268 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import Foundation + +/// Failure while encoding or decoding wire bytes. Internal to the codec; the +/// client maps it onto an ``IggyError`` at the API boundary. +enum WireError: Error, Equatable { + case truncated(offset: Int, need: Int, have: Int) + case invalidUTF8(offset: Int) + case invalidDiscriminant(type: String, value: UInt8) + case validation(String) + case invalidBatchChecksum(stored: UInt64, computed: UInt64) + case invalidMessageChecksum(stored: UInt64, computed: UInt64, offset: UInt64) + case invalidMessageTimestampDelta(UInt64) + + var iggyError: IggyError { Review Comment: warning: this maps a wire failure to `invalidFormat` (4), while the Rust client maps the same class to `InvalidCommand` (3). a caller matching on the code sees a different value in each SDK - intended? ########## foreign/swift/README.md: ########## @@ -0,0 +1,28 @@ +# Swift SDK for Iggy Review Comment: nit: no logo block and no install or usage section, unlike the other foreign SDK READMEs. add both so a reader can add the package. ########## foreign/swift/Sources/Iggy/Wire/ByteCodec.swift: ########## @@ -0,0 +1,268 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import Foundation + +/// Failure while encoding or decoding wire bytes. Internal to the codec; the +/// client maps it onto an ``IggyError`` at the API boundary. +enum WireError: Error, Equatable { Review Comment: simplification: `WireError` carries four cases the codec never throws, plus an `iggyError` mapping with no caller. keep the three thrown cases and delete the mapping. ########## foreign/swift/Sources/Iggy/Wire/ByteCodec.swift: ########## @@ -0,0 +1,268 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import Foundation + +/// Failure while encoding or decoding wire bytes. Internal to the codec; the +/// client maps it onto an ``IggyError`` at the API boundary. +enum WireError: Error, Equatable { + case truncated(offset: Int, need: Int, have: Int) + case invalidUTF8(offset: Int) + case invalidDiscriminant(type: String, value: UInt8) + case validation(String) + case invalidBatchChecksum(stored: UInt64, computed: UInt64) + case invalidMessageChecksum(stored: UInt64, computed: UInt64, offset: UInt64) + case invalidMessageTimestampDelta(UInt64) + + var iggyError: IggyError { + switch self { + case .truncated(let offset, let need, let have): + IggyError(.invalidFormat, context: "unexpected end of buffer at offset \(offset): need \(need) bytes, have \(have)") + case .invalidUTF8(let offset): + IggyError(.invalidUtf8, context: "invalid utf-8 at offset \(offset)") + case .invalidDiscriminant(let type, let value): + IggyError(.invalidFormat, context: "unknown discriminant \(value) for \(type)") + case .validation(let message): + IggyError(.invalidFormat, context: message) + case .invalidBatchChecksum(let stored, let computed): + IggyError(.invalidBatchChecksum, context: "stored \(stored), computed \(computed)") + case .invalidMessageChecksum(let stored, let computed, let offset): + IggyError(.invalidMessageChecksum, context: "stored \(stored), computed \(computed), offset \(offset)") + case .invalidMessageTimestampDelta(let delta): + IggyError(.invalidMessageTimestampDelta, context: "delta \(delta) microseconds") + } + } +} + +/// Little-endian append-only encoder over a byte array. +struct ByteWriter { + private(set) var bytes: [UInt8] + + init(capacity: Int = 0) { + bytes = [] + bytes.reserveCapacity(capacity) + } + + var count: Int { bytes.count } + + mutating func write(_ value: UInt8) { + bytes.append(value) + } + + mutating func write(_ value: Bool) { + bytes.append(value ? 1 : 0) + } + + mutating func write(_ value: UInt16) { + withUnsafeBytes(of: value.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func write(_ value: UInt32) { + withUnsafeBytes(of: value.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func write(_ value: UInt64) { + withUnsafeBytes(of: value.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func write(_ value: Float) { + write(value.bitPattern) + } + + mutating func write(_ value: UInt128Value) { + write(value.low) + write(value.high) + } + + mutating func write(_ value: [UInt8]) { + bytes.append(contentsOf: value) + } + + mutating func write(_ value: ArraySlice<UInt8>) { + bytes.append(contentsOf: value) + } + + mutating func write(_ value: String) { + bytes.append(contentsOf: value.utf8) + } + + /// `[len: u8][utf8]`, the layout of every wire name. The caller guarantees + /// the length fits. + mutating func writeName(_ value: String) { + let utf8 = Array(value.utf8) + bytes.append(UInt8(utf8.count)) + bytes.append(contentsOf: utf8) + } + + /// `[len: u32][utf8]`, the layout of longer free-form strings. + mutating func writeLongString(_ value: String) { + let utf8 = Array(value.utf8) + write(UInt32(utf8.count)) + bytes.append(contentsOf: utf8) + } + + mutating func writeZeros(_ count: Int) { Review Comment: simplification: `writeZeros`, `overwrite` and `count` here, plus `skip` and `rest`, have no caller in this tree. add them in the PR that first needs them. also at lines 229-235. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
