RustToMetal commented on code in PR #4190: URL: https://github.com/apache/iggy/pull/4190#discussion_r4067711664
########## 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: Done, both halves go through withUnsafeBytes now and random draws two UInt64s from the system generator. ########## 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: Done, encodedSize lands with the first encoders in the models PR and the batch encoder sizes the writer from it, so this PR keeps only the reserve. ########## 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: Done, the comment names the server source file and a table test pins the count, both ends, and a code from every band. ########## 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: Done, added a build-macos lane on macos-15 that builds and tests with the Xcode toolchain. ########## 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: Done, overwrite moves to the protocol PR with a doc that describes 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: Done, the doc states the 16 byte requirement and that anything else traps. ########## .github/config/components.yml: ########## @@ -443,6 +455,7 @@ components: - "bdd/scenarios/**" tasks: ["bdd-cpp"] + Review Comment: Done, both blank lines are gone. ########## 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: Done, equality and hashing compare code and rawCode only now, and the doc says context is excluded. ########## 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: Done, marked provisional in the doc, the release wiring in the last PR of the series reads it. ########## .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: Done, added examples/swift and bdd/swift to the paths. -- 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]
