diff --git a/swift/core/Sources/A2UICore/Extensions/JSONValue+Path.swift b/swift/core/Sources/A2UICore/Extensions/JSONValue+Path.swift index 2cce0d4f04..1fbe0355b7 100644 --- a/swift/core/Sources/A2UICore/Extensions/JSONValue+Path.swift +++ b/swift/core/Sources/A2UICore/Extensions/JSONValue+Path.swift @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import OrderedJSON import OrderedCollections +import OrderedJSON extension JSONValue { /// Returns the underlying string value if this is a `.string` case. @@ -40,10 +40,7 @@ extension JSONValue { switch self { case .integer(let value): return value case .number(let value): - if value >= Double(Int.min) && value <= Double(Int.max) && value == value.rounded() { - return Int(value) - } - return nil + return Int(exactly: value) default: return nil } } @@ -77,7 +74,8 @@ extension JSONValue { /// if this is an `.object` case. public var dictionaryValue: [String: JSONValue]? { switch self { - case .object(let value): return Dictionary(uniqueKeysWithValues: value.map { ($0.key, $0.value) }) + case .object(let value): + return Dictionary(uniqueKeysWithValues: value.map { ($0.key, $0.value) }) default: return nil } } diff --git a/swift/core/Sources/A2UICore/Messages/ClientToServerMessage.swift b/swift/core/Sources/A2UICore/Messages/ClientToServerMessage.swift index 04ac104ea2..c708ffe3c3 100644 --- a/swift/core/Sources/A2UICore/Messages/ClientToServerMessage.swift +++ b/swift/core/Sources/A2UICore/Messages/ClientToServerMessage.swift @@ -68,4 +68,3 @@ public enum ClientToServerMessage: Equatable, Codable, Sendable { } } } - diff --git a/swift/core/Sources/A2UICore/Messages/ServerToClientMessage.swift b/swift/core/Sources/A2UICore/Messages/ServerToClientMessage.swift index 01c8cb58d5..4bae816da0 100644 --- a/swift/core/Sources/A2UICore/Messages/ServerToClientMessage.swift +++ b/swift/core/Sources/A2UICore/Messages/ServerToClientMessage.swift @@ -90,4 +90,3 @@ public enum ServerToClientMessage: Codable, Sendable, Equatable { } } } - diff --git a/swift/core/Sources/A2UICore/Models/SurfaceGroupModel.swift b/swift/core/Sources/A2UICore/Models/SurfaceGroupModel.swift new file mode 100644 index 0000000000..90e7fc57a5 --- /dev/null +++ b/swift/core/Sources/A2UICore/Models/SurfaceGroupModel.swift @@ -0,0 +1,92 @@ +// Copyright 2026 Google LLC +// +// Licensed 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 +// +// https://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 Combine +import Foundation +import OrderedCollections +import OrderedJSON + +/// The root state model managing the collection of active surfaces. +/// +/// `SurfaceGroupModel` owns the surface dictionary, lifecycle +/// (add/remove), and cross-surface aggregation such as +/// `sendDataModel` tracking. It mirrors the `SurfaceGroupModel` type +/// in the `web_core` reference implementation. +@MainActor +public final class SurfaceGroupModel: ObservableObject { + private var surfaces: [String: SurfaceViewModel] = [:] + private var sendDataModelSurfaces: Set = [] + + /// The dictionary of active surfaces, published to the UI. + @Published public private(set) var surfacesMap: [String: SurfaceViewModel] = [:] + + public init() {} + + // MARK: - Surface Lifecycle + + /// Adds a surface to the group. + /// + /// If a surface with the same ID already exists, the call is + /// silently ignored (matching `web_core`'s behavior). + public func addSurface(_ vm: SurfaceViewModel) { + guard surfaces[vm.surfaceID] == nil else { return } + surfaces[vm.surfaceID] = vm + surfacesMap = surfaces + } + + /// Removes a surface from the group by its ID. + public func removeSurface(id: String) { + guard surfaces[id] != nil else { return } + surfaces.removeValue(forKey: id) + sendDataModelSurfaces.remove(id) + surfacesMap = surfaces + } + + // MARK: - Surface Lookup + + /// Retrieves a surface by its ID. + public func surface(id: String) -> SurfaceViewModel? { + surfaces[id] + } + + /// Returns a snapshot of all active surfaces. + public func allSurfaces() -> [String: SurfaceViewModel] { + surfaces + } + + // MARK: - send Data Model + + /// Marks a surface as requesting data-model reporting. + public func setSendDataModel(surfaceID: String, enabled: Bool) { + if enabled { + sendDataModelSurfaces.insert(surfaceID) + } else { + sendDataModelSurfaces.remove(surfaceID) + } + } + + /// Aggregates the data models of all surfaces that have + /// `sendDataModel` enabled. + /// + /// Returns `nil` if no surfaces have the flag set. + public func getClientDataModel() -> JSONValue? { + var result: OrderedDictionary = [:] + for surfaceID in sendDataModelSurfaces { + guard let vm = surfaces[surfaceID] else { continue } + result[surfaceID] = vm.dataModel.snapshot() + } + guard !result.isEmpty else { return nil } + return .object(result) + } +} diff --git a/swift/core/Sources/A2UICore/Models/SurfaceTheme.swift b/swift/core/Sources/A2UICore/Models/SurfaceTheme.swift index 11c6ba261c..99959b87f0 100644 --- a/swift/core/Sources/A2UICore/Models/SurfaceTheme.swift +++ b/swift/core/Sources/A2UICore/Models/SurfaceTheme.swift @@ -14,4 +14,3 @@ /// A marker protocol representing catalog-defined visual theme parameters. public protocol SurfaceTheme: Sendable {} - diff --git a/swift/core/Sources/A2UICore/Models/SurfaceViewModel.swift b/swift/core/Sources/A2UICore/Models/SurfaceViewModel.swift index af3edd1d66..a5b0f2ea21 100644 --- a/swift/core/Sources/A2UICore/Models/SurfaceViewModel.swift +++ b/swift/core/Sources/A2UICore/Models/SurfaceViewModel.swift @@ -107,7 +107,8 @@ public final class SurfaceViewModel: @unchecked Sendable, ObservableObject { // "...#/$defs/DynamicString") and match exactly to avoid // misidentifying types like "DynamicStringList" as "DynamicString". if let ref = schemaJSON["$ref"]?.stringValue { - let typeName = ref + let typeName = + ref .split(separator: "/") .last .map(String.init) diff --git a/swift/core/Sources/A2UICore/Processing/MessageErrorMapper.swift b/swift/core/Sources/A2UICore/Processing/MessageErrorMapper.swift new file mode 100644 index 0000000000..7ae70d834a --- /dev/null +++ b/swift/core/Sources/A2UICore/Processing/MessageErrorMapper.swift @@ -0,0 +1,136 @@ +// Copyright 2026 Google LLC +// +// Licensed 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 +// +// https://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 + +/// Converts internal Swift errors (e.g. `DecodingError`) into +/// spec-compliant `ClientServerError` values suitable for sending +/// to the server. +/// +/// This type encapsulates the mapping logic that was previously +/// inlined in `MessageProcessor.handleError`. It is a value type +/// with no mutable state, making it safe to share across threads. +public struct MessageErrorMapper: Sendable { + public init() {} + + /// Maps an error to a `ClientServerError` suitable for the + /// client-to-server `error` message. + /// + /// - Parameters: + /// - error: The internal error to convert. + /// - surfaceID: The surface ID to attribute the error to. + /// - Returns: A `ClientServerError` matching the v0.9.1 wire + /// format. + public func map( + _ error: Error, + surfaceID: String + ) -> ClientServerError { + if let genericError = error as? GenericError { + return .generic(genericError) + } + + if let decodingError = error as? DecodingError { + return mapDecodingError(decodingError, surfaceID: surfaceID) + } + + return .generic( + GenericError( + code: "PARSING_FAILED", + surfaceID: surfaceID, + message: error.localizedDescription + ) + ) + } + + // MARK: - DecodingError Mapping + + private func mapDecodingError( + _ error: DecodingError, + surfaceID: String + ) -> ClientServerError { + let codingPath = resolveCodingPath(from: error) + let description = resolveDecodingErrorDescription(error) + + switch error { + case .typeMismatch, .valueNotFound, .keyNotFound: + return .validationFailed( + ValidationFailedError( + surfaceID: surfaceID, + path: codingPath, + message: description + ) + ) + case .dataCorrupted: + return .generic( + GenericError( + code: "PARSING_FAILED", + surfaceID: surfaceID, + message: description + ) + ) + @unknown default: + return .generic( + GenericError( + code: "PARSING_FAILED", + surfaceID: surfaceID, + message: description + ) + ) + } + } + + private func resolveCodingPath( + from error: DecodingError + ) -> String { + let codingPath: [CodingKey] + switch error { + case .typeMismatch(_, let context), + .valueNotFound(_, let context), + .keyNotFound(_, let context), + .dataCorrupted(let context): + codingPath = context.codingPath + @unknown default: + codingPath = [] + } + + guard !codingPath.isEmpty else { return "" } + let pointer = codingPath.map { key in + if let intVal = key.intValue { + return String(intVal) + } else { + return key.stringValue + .replacingOccurrences(of: "~", with: "~0") + .replacingOccurrences(of: "/", with: "~1") + } + }.joined(separator: "/") + return "/" + pointer + } + + private func resolveDecodingErrorDescription( + _ error: DecodingError + ) -> String { + switch error { + case .typeMismatch(let type, let context): + return "Type mismatch: expected type '\(type)', got '\(context.debugDescription)'" + case .valueNotFound(let type, let context): + return "Missing value: expected non-nil '\(type)', got '\(context.debugDescription)'" + case .keyNotFound(let key, let context): + return "Missing required key '\(key.stringValue)': \(context.debugDescription)" + case .dataCorrupted(let context): + return "JSON syntax error: \(context.debugDescription)" + @unknown default: + return "Unknown decoding error" + } + } +} diff --git a/swift/core/Sources/A2UICore/Processing/MessageParser.swift b/swift/core/Sources/A2UICore/Processing/MessageParser.swift new file mode 100644 index 0000000000..de934050ca --- /dev/null +++ b/swift/core/Sources/A2UICore/Processing/MessageParser.swift @@ -0,0 +1,63 @@ +// Copyright 2026 Google LLC +// +// Licensed 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 +// +// https://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 + +/// A thread-safe parser for decoding A2UI server-to-client messages. +public final class MessageParser: Sendable { + private let decoder: JSONDecoder + + public init(decoder: JSONDecoder = JSONDecoder()) { + self.decoder = decoder + } + + /// Parses a `ServerToClientMessage` from a JSON-encoded string. + public func parse(jsonString: String) throws -> ServerToClientMessage { + let data = Data(jsonString.utf8) + return try decode(jsonData: data) + } + + /// Decodes a `ServerToClientMessage` from raw JSON data. + public func decode(jsonData: Data) throws -> ServerToClientMessage { + try decoder.decode(ServerToClientMessage.self, from: jsonData) + } + + /// Best-effort extraction of a `surfaceId` from a raw JSON line. + /// + /// This is used as a fallback when `parse(jsonString:)` fails, so + /// the caller can still attribute the error to the correct + /// surface. Returns `nil` if the JSON is malformed or contains no + /// `surfaceId` field. + public func extractSurfaceID(fromLine line: String) -> String? { + guard let data = line.data(using: .utf8), + let dict = try? JSONSerialization.jsonObject(with: data) + as? [String: Any] + else { + return nil + } + + for value in dict.values { + guard let subDict = value as? [String: Any], + let rawID = subDict["surfaceId"] + else { continue } + + if let strID = rawID as? String { + return strID + } else if let numID = rawID as? NSNumber { + return numID.stringValue + } + } + return nil + } +} diff --git a/swift/core/Sources/A2UICore/Processing/MessageProcessor.swift b/swift/core/Sources/A2UICore/Processing/MessageProcessor.swift index 7a03d9f59c..47f5c623dd 100644 --- a/swift/core/Sources/A2UICore/Processing/MessageProcessor.swift +++ b/swift/core/Sources/A2UICore/Processing/MessageProcessor.swift @@ -12,138 +12,316 @@ // See the License for the specific language governing permissions and // limitations under the License. +import Combine import Foundation import JSONSchema import OrderedCollections import OrderedJSON -/// The central processor for A2UI messages. +/// The central processor for A2UI server-to-client messages. /// /// Mirrors `MessageProcessor` in the core blueprint and `web_core`. -/// Accepts strongly-typed ``ServerToClientMessage`` values, validates -/// component declarations against the catalog's schemas, and mutates -/// the corresponding ``SurfaceViewModel`` state. +/// Accepts strongly-typed ``ServerToClientMessage`` values or raw JSON lines, +///// The central processor for A2UI server-to-client messages. /// -/// This is the natural extension point for multi-version support: -/// version-specific parsing and validation logic lives here, keeping -/// the state models (`SurfaceViewModel`, `SurfaceComponentsModel`, -/// `DataModel`) clean and version-agnostic. -public final class MessageProcessor: @unchecked Sendable { - - private let lock = NSRecursiveLock() - private var surfaces: [String: SurfaceViewModel] = [:] - private let catalogs: [Catalog] - private let actionHandler: (any ActionHandling)? - - /// Creates a new message processor. - /// - /// - Parameters: - /// - catalogs: The available catalogs for component validation. - /// - actionHandler: An optional global handler for actions from all surfaces. +/// Mirrors `MessageProcessor` in the core blueprint and `web_core`. +/// Accepts strongly-typed ``ServerToClientMessage`` values or raw JSON lines, +/// validates component declarations against catalog schemas, and mutates +/// the corresponding ``SurfaceViewModel`` state via ``SurfaceGroupModel``. +@MainActor +public final class MessageProcessor: ObservableObject { + /// The surface group model owning all active surfaces. + public let surfaceGroupModel: SurfaceGroupModel + + private let catalogs: [String: Catalog] + private weak var actionHandler: (any ActionHandling)? + private let parser = MessageParser() + private let errorMapper = MessageErrorMapper() + + /// Creates a new message processor with an array of catalogs. public init( catalogs: [Catalog], actionHandler: (any ActionHandling)? = nil + ) { + self.catalogs = Dictionary( + catalogs.map { ($0.id, $0) }, + uniquingKeysWith: { _, last in last } + ) + self.actionHandler = actionHandler + self.surfaceGroupModel = SurfaceGroupModel() + } + + /// Creates a new message processor with a dictionary of catalogs. + public init( + catalogs: [String: Catalog], + actionHandler: (any ActionHandling)? = nil ) { self.catalogs = catalogs self.actionHandler = actionHandler + self.surfaceGroupModel = SurfaceGroupModel() } - // MARK: - Surface Management + // MARK: - Surface Lookup & Management /// Returns the surface model for the given ID, if it exists. public func getSurface(_ id: String) -> SurfaceViewModel? { - lock.withLock { surfaces[id] } + surfaceGroupModel.surface(id: id) } - /// Creates a new surface with the given ID and catalog. - /// - /// - Parameters: - /// - surfaceID: The unique surface identifier. - /// - catalog: The catalog to use for this surface. - /// - Returns: The newly created `SurfaceViewModel`. + /// Creates a surface using a catalog ID lookup. + public func createSurface( + surfaceID: String, + catalogID: String + ) -> SurfaceViewModel? { + guard let catalog = catalogs[catalogID] else { return nil } + return createSurface(surfaceID: surfaceID, catalog: catalog) + } + + /// Creates a surface directly with a given catalog instance. + @discardableResult public func createSurface( surfaceID: String, catalog: Catalog ) -> SurfaceViewModel { - lock.withLock { - let surface = SurfaceViewModel( - surfaceID: surfaceID, - catalog: catalog, - actionHandler: actionHandler - ) - surfaces[surfaceID] = surface - return surface + let vm = SurfaceViewModel( + surfaceID: surfaceID, + catalog: catalog, + actionHandler: actionHandler + ) + surfaceGroupModel.addSurface(vm) + return vm + } + + /// Deletes the surface with the given ID. + public func deleteSurface(_ surfaceID: String) { + surfaceGroupModel.removeSurface(id: surfaceID) + } + + /// Returns a snapshot of all active surfaces. + public func allSurfaces() -> [String: SurfaceViewModel] { + surfaceGroupModel.allSurfaces() + } + + /// Returns the aggregated data model for surfaces with `sendDataModel` enabled. + public func getClientDataModel() -> JSONValue? { + surfaceGroupModel.getClientDataModel() + } + + // MARK: - Capabilities Generation + + /// Options for generating client capabilities. + public struct CapabilitiesOptions: Sendable { + /// If true, full definitions of all catalogs will be included as inline catalogs. + public var includeInlineCatalogs: Bool + + /// The protocol version to generate capabilities for (defaults to "v0.9.1"). + public var version: String + + public init( + includeInlineCatalogs: Bool = false, + version: String = "v0.9.1" + ) { + self.includeInlineCatalogs = includeInlineCatalogs + self.version = version } } - /// Creates a surface using a catalog ID lookup. + /// Generates the `a2uiClientCapabilities` object for all registered catalogs. /// - /// - Parameters: - /// - surfaceID: The unique surface identifier. - /// - catalogID: The ID of the catalog to use. - /// - Returns: The newly created `SurfaceViewModel`, or `nil` if the catalog - /// is not found. - public func createSurface( - surfaceID: String, - catalogID: String - ) -> SurfaceViewModel? { - guard let catalog = catalogs.first(where: { $0.id == catalogID }) else { + /// - Parameter options: Configuration options for capability generation. + /// - Returns: A `JSONValue` representing the capabilities structure. + public func getClientCapabilities( + options: CapabilitiesOptions = CapabilitiesOptions() + ) -> JSONValue { + let supportedCatalogIDs = Array(catalogs.keys).sorted() + var versionCaps: OrderedDictionary = [ + "supportedCatalogIds": .array(supportedCatalogIDs.map { .string($0) }) + ] + + if options.includeInlineCatalogs { + let inlineCatalogs = catalogs.values.map { generateInlineCatalog($0) } + versionCaps["inlineCatalogs"] = .array(inlineCatalogs) + } + + return .object([ + options.version: .object(versionCaps) + ]) + } + + private func generateInlineCatalog(_ catalog: Catalog) -> JSONValue { + var componentsDictionary: OrderedDictionary = [:] + + for (name, componentAPI) in catalog.components { + let schemaJSON = schemaToJSONValue(componentAPI.schema) ?? .object([:]) + let processedSchema = processRefs(schemaJSON) + + var properties: OrderedDictionary = [ + "component": .object(["const": .string(name)]) + ] + var required: [JSONValue] = [.string("component")] + + if let originalProperties = processedSchema["properties"]?.objectValue { + for (key, value) in originalProperties { + properties[key] = value + } + } + if let originalRequired = processedSchema["required"]?.arrayValue { + for requiredProperty in originalRequired { + if !required.contains(requiredProperty) { + required.append(requiredProperty) + } + } + } + + let componentSchema: JSONValue = .object([ + "allOf": .array([ + .object(["$ref": .string("common_types.json#/$defs/ComponentCommon")]), + .object([ + "properties": .object(properties), + "required": .array(required), + ]), + ]) + ]) + componentsDictionary[name] = componentSchema + } + + var functionsArray: [JSONValue] = [] + for (_, functionImplementation) in catalog.functions { + let functionAPI = functionImplementation.api + let schemaJSON = schemaToJSONValue(functionAPI.schema) ?? .object([:]) + let processedParameters = processRefs(schemaJSON) + + var functionDictionary: OrderedDictionary = [ + "name": .string(functionAPI.name), + "returnType": .string(functionAPI.returnType.rawValue), + ] + if let functionDescription = processedParameters["description"]?.stringValue { + functionDictionary["description"] = .string(functionDescription) + } + functionDictionary["parameters"] = processedParameters + functionsArray.append(.object(functionDictionary)) + } + + var catalogDictionary: OrderedDictionary = [ + "catalogId": .string(catalog.id), + "components": .object(componentsDictionary), + ] + if !functionsArray.isEmpty { + catalogDictionary["functions"] = .array(functionsArray) + } + + if let themeSchema = catalog.themeSchema { + let schemaJSON = schemaToJSONValue(themeSchema) ?? .object([:]) + let processedTheme = processRefs(schemaJSON) + if let themeProperties = processedTheme["properties"] { + catalogDictionary["theme"] = themeProperties + } else { + catalogDictionary["theme"] = processedTheme + } + } + + return .object(catalogDictionary) + } + + private func schemaToJSONValue(_ schema: Schema) -> JSONValue? { + let encoder = JSONEncoder() + guard let data = try? encoder.encode(schema), + let json = try? JSONValue.parse(data) + else { return nil } - return createSurface(surfaceID: surfaceID, catalog: catalog) + return json } - /// Deletes the surface with the given ID. - public func deleteSurface(_ surfaceID: String) { - _ = lock.withLock { - surfaces.removeValue(forKey: surfaceID) + private func processRefs(_ value: JSONValue) -> JSONValue { + switch value { + case .object(let dict): + if let desc = dict["description"]?.stringValue, desc.hasPrefix("REF:") { + let parts = desc.dropFirst(4).split(separator: "|") + let refPart = parts.first.map(String.init) ?? "" + let customDescription = parts.count > 1 ? String(parts[1]) : nil + var resultDict: OrderedDictionary = ["$ref": .string(refPart)] + if let customDescription, !customDescription.isEmpty { + resultDict["description"] = .string(customDescription) + } + return .object(resultDict) + } + var newDict: OrderedDictionary = [:] + for (k, v) in dict { + newDict[k] = processRefs(v) + } + return .object(newDict) + + case .array(let arr): + return .array(arr.map { processRefs($0) }) + + default: + return value } } - // MARK: - Message Processing + // MARK: - Message Processing (JSONL Line) - /// Processes a single server-to-client message. + /// Processes a single JSONL line containing an incoming message envelope. /// - /// - Parameter message: The message to process. + /// Throws on any failure (decoding error, missing surface, missing catalog). + /// Thrown parsing errors are also routed to `ActionHandling` via `MessageErrorMapper`. + public func process(line: String) throws { + let surfaceID = parser.extractSurfaceID(fromLine: line) ?? "unknown" + do { + let message = try parser.parse(jsonString: line) + try validateAndProcess(message) + } catch { + let clientError = errorMapper.map(error, surfaceID: surfaceID) + actionHandler?.handle(error: clientError, from: surfaceID) + throw error + } + } + + // MARK: - Message Processing (Strongly-Typed) + + /// Processes a single server-to-client message. public func processMessage(_ message: ServerToClientMessage) { - switch message { - case .createSurface(let msg): - processCreateSurface(msg) - case .updateComponents(let msg): - processUpdateComponents(msg) - case .updateDataModel(let msg): - processUpdateDataModel(msg) - case .deleteSurface(let msg): - processDeleteSurface(msg) + do { + try validateAndProcess(message) + } catch { + let surfaceID: String + switch message { + case .createSurface(let msg): surfaceID = msg.surfaceID + case .updateComponents(let msg): surfaceID = msg.surfaceID + case .updateDataModel(let msg): surfaceID = msg.surfaceID + case .deleteSurface(let msg): surfaceID = msg.surfaceID + } + let clientError = errorMapper.map(error, surfaceID: surfaceID) + actionHandler?.handle(error: clientError, from: surfaceID) } } /// Processes an array of server-to-client messages. - /// - /// - Parameter messages: The messages to process. public func processMessages(_ messages: [ServerToClientMessage]) { for message in messages { processMessage(message) } } - // MARK: - Direct Mutation API + // MARK: - Direct Surface Mutations & Validation - /// Updates components on a surface, validating each against the - /// catalog's schema. - /// - /// Valid components are stored in the surface's `SurfaceComponentsModel` - /// as ``ComponentModel`` instances. Validation errors are reported to - /// the surface's action handler. - /// - /// - Parameters: - /// - surfaceID: The surface to update. - /// - components: The raw component dictionaries to validate and store. + /// Updates components on a surface, validating each against the catalog's schema. public func updateComponents( surfaceID: String, components: [[String: JSONValue]] ) { - guard let surface = getSurface(surfaceID) else { return } + guard let surface = surfaceGroupModel.surface(id: surfaceID) else { + let error = ClientServerError.generic( + GenericError( + code: "SURFACE_NOT_FOUND", + surfaceID: surfaceID, + message: "Surface not found: \(surfaceID)" + ) + ) + actionHandler?.handle(error: error, from: surfaceID) + return + } for componentDict in components { guard let type = componentDict["component"]?.stringValue else { @@ -190,7 +368,6 @@ public final class MessageProcessor: @unchecked Sendable { let result = schema.validate(instance) if result.isValid { - // Extract properties (excluding id and component) var props: [String: JSONValue] = [:] for (key, val) in componentDict where key != "id" && key != "component" { props[key] = val @@ -198,16 +375,11 @@ public final class MessageProcessor: @unchecked Sendable { let existing = surface.componentsModel.get(id) if existing != nil && existing?.type != type { - // Type changed: recreate the component + // Component type changed: recreate component to reset state surface.componentsModel.removeComponent(id) surface.componentsModel.addComponent( ComponentModel(id: id, type: type, properties: props) ) - } else if existing != nil { - // Update properties in place - surface.componentsModel.addComponent( - ComponentModel(id: id, type: type, properties: props) - ) } else { surface.componentsModel.addComponent( ComponentModel(id: id, type: type, properties: props) @@ -231,67 +403,89 @@ public final class MessageProcessor: @unchecked Sendable { } /// Updates a path in a surface's data model. - /// - /// - Parameters: - /// - surfaceID: The surface to update. - /// - path: The JSON Pointer path. - /// - value: The value to set, or `nil` to remove. public func updateDataModel( surfaceID: String, path: String, value: JSONValue? ) { - guard let surface = getSurface(surfaceID) else { return } + guard let surface = surfaceGroupModel.surface(id: surfaceID) else { + let error = ClientServerError.generic( + GenericError( + code: "SURFACE_NOT_FOUND", + surfaceID: surfaceID, + message: "Surface not found: \(surfaceID)" + ) + ) + actionHandler?.handle(error: error, from: surfaceID) + return + } surface.dataModel.set(path, value: value) surface.rebuildTree() } - // MARK: - Private Message Handlers + // MARK: - Private Validation & Processing - private func processCreateSurface(_ msg: CreateSurfaceMessage) { - guard surfaces[msg.surfaceID] == nil else { - let error = ClientServerError.generic( - GenericError( + private func validateAndProcess(_ message: ServerToClientMessage) throws { + switch message { + case .createSurface(let msg): + guard surfaceGroupModel.surface(id: msg.surfaceID) == nil else { + let error = GenericError( code: "SURFACE_EXISTS", surfaceID: msg.surfaceID, message: "Surface \(msg.surfaceID) already exists." ) - ) - actionHandler?.handle(error: error, from: msg.surfaceID) - return - } - - guard let catalog = catalogs.first(where: { $0.id == msg.catalogID }) else { - let error = ClientServerError.generic( - GenericError( + throw error + } + guard let catalog = catalogs[msg.catalogID] else { + let error = GenericError( code: "CATALOG_NOT_FOUND", surfaceID: msg.surfaceID, message: "Catalog not found: \(msg.catalogID)" ) + throw error + } + let vm = SurfaceViewModel( + surfaceID: msg.surfaceID, + catalog: catalog, + actionHandler: actionHandler ) - actionHandler?.handle(error: error, from: msg.surfaceID) - return - } - - _ = createSurface(surfaceID: msg.surfaceID, catalog: catalog) - } + if msg.shouldSendDataModel { + surfaceGroupModel.setSendDataModel(surfaceID: msg.surfaceID, enabled: true) + } + surfaceGroupModel.addSurface(vm) - private func processUpdateComponents(_ msg: UpdateComponentsMessage) { - updateComponents( - surfaceID: msg.surfaceID, - components: msg.components - ) - } + case .updateComponents(let msg): + guard surfaceGroupModel.surface(id: msg.surfaceID) != nil else { + let error = GenericError( + code: "SURFACE_NOT_FOUND", + surfaceID: msg.surfaceID, + message: "Surface not found: \(msg.surfaceID)" + ) + throw error + } + updateComponents(surfaceID: msg.surfaceID, components: msg.components) - private func processUpdateDataModel(_ msg: UpdateDataModelMessage) { - updateDataModel( - surfaceID: msg.surfaceID, - path: msg.path, - value: msg.value - ) - } + case .updateDataModel(let msg): + guard surfaceGroupModel.surface(id: msg.surfaceID) != nil else { + let error = GenericError( + code: "SURFACE_NOT_FOUND", + surfaceID: msg.surfaceID, + message: "Surface not found: \(msg.surfaceID)" + ) + throw error + } + updateDataModel(surfaceID: msg.surfaceID, path: msg.path, value: msg.value) - private func processDeleteSurface(_ msg: DeleteSurfaceMessage) { - deleteSurface(msg.surfaceID) + case .deleteSurface(let msg): + guard surfaceGroupModel.surface(id: msg.surfaceID) != nil else { + let error = GenericError( + code: "SURFACE_NOT_FOUND", + surfaceID: msg.surfaceID, + message: "Surface not found: \(msg.surfaceID)" + ) + throw error + } + surfaceGroupModel.removeSurface(id: msg.surfaceID) + } } } diff --git a/swift/core/Tests/A2UICoreTests/ClientToServerMessageTests.swift b/swift/core/Tests/A2UICoreTests/ClientToServerMessageTests.swift index 6722d84a25..79c143f5c1 100644 --- a/swift/core/Tests/A2UICoreTests/ClientToServerMessageTests.swift +++ b/swift/core/Tests/A2UICoreTests/ClientToServerMessageTests.swift @@ -22,7 +22,8 @@ struct ClientToServerMessageTests { // MARK: - Decoding @Test func decodeValidAction() throws { - let json = try #require(""" + let json = try #require( + """ { "version": "v0.9.1", "action": { @@ -49,7 +50,8 @@ struct ClientToServerMessageTests { } @Test func decodeValidActionWithEmptyContext() throws { - let json = try #require(""" + let json = try #require( + """ { "version": "v0.9.1", "action": { @@ -73,7 +75,8 @@ struct ClientToServerMessageTests { } @Test func decodeValidError() throws { - let json = try #require(""" + let json = try #require( + """ { "version": "v0.9.1", "error": { @@ -122,7 +125,8 @@ struct ClientToServerMessageTests { } @Test func decodeRejectsActionMissingRequiredField() throws { - let json = try #require(""" + let json = try #require( + """ { "version": "v0.9.1", "action": { diff --git a/swift/core/Tests/A2UICoreTests/JSONValuePathTests.swift b/swift/core/Tests/A2UICoreTests/JSONValuePathTests.swift index 3c01d81d97..e5fd3f89d9 100644 --- a/swift/core/Tests/A2UICoreTests/JSONValuePathTests.swift +++ b/swift/core/Tests/A2UICoreTests/JSONValuePathTests.swift @@ -86,7 +86,7 @@ struct JSONValuePathTests { @Test func subscriptGetReturnsNestedValue() { let value: JSONValue = [ - "user": ["name": "Alice"], + "user": ["name": "Alice"] ] #expect(value["user/name"]?.stringValue == "Alice") } @@ -98,21 +98,21 @@ struct JSONValuePathTests { @Test func subscriptGetReturnsArrayElement() { let value: JSONValue = [ - "items": ["a", "b", "c"], + "items": ["a", "b", "c"] ] #expect(value["items/1"]?.stringValue == "b") } @Test func subscriptGetReturnsNilForInvalidArrayIndex() { let value: JSONValue = [ - "items": ["a", "b"], + "items": ["a", "b"] ] #expect(value["items/5"] == nil) } @Test func subscriptGetHandlesDeepNesting() { let value: JSONValue = [ - "a": ["b": ["c": ["d": "deep"]]], + "a": ["b": ["c": ["d": "deep"]]] ] #expect(value["a/b/c/d"]?.stringValue == "deep") } @@ -146,7 +146,7 @@ struct JSONValuePathTests { @Test func subscriptSetAppendsToArray() { var value: JSONValue = [ - "items": [1, 2, 3], + "items": [1, 2, 3] ] value["items/3"] = 4 #expect(value["items"]?.arrayValue?.count == 4) @@ -155,7 +155,7 @@ struct JSONValuePathTests { @Test func subscriptSetReplacesArrayElement() { var value: JSONValue = [ - "items": [1, 2, 3], + "items": [1, 2, 3] ] value["items/1"] = 99 #expect(value["items/1"]?.intValue == 99) @@ -170,7 +170,7 @@ struct JSONValuePathTests { @Test func subscriptSetDeletesByArrayIndex() { var value: JSONValue = [ - "items": [1, 2, 3], + "items": [1, 2, 3] ] value["items/1"] = nil // Setting an array index to nil preserves length (sparse array) @@ -182,7 +182,7 @@ struct JSONValuePathTests { @Test func subscriptSetAutoVivifiesObjectFromArray() { var value: JSONValue = [ - "items": [["name": "Alice"]], + "items": [["name": "Alice"]] ] value["items/0/age"] = 30 #expect(value["items/0/age"]?.intValue == 30) diff --git a/swift/core/Tests/A2UICoreTests/MessageErrorMapperTests.swift b/swift/core/Tests/A2UICoreTests/MessageErrorMapperTests.swift new file mode 100644 index 0000000000..2773f73e77 --- /dev/null +++ b/swift/core/Tests/A2UICoreTests/MessageErrorMapperTests.swift @@ -0,0 +1,68 @@ +// Copyright 2026 Google LLC +// +// Licensed 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 +// +// https://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 A2UICore +import Foundation +import Testing + +struct MessageErrorMapperTests { + + private let mapper = MessageErrorMapper() + + @Test func mapGenericError() { + let error = GenericError( + code: "TEST_ERROR", + surfaceID: "s1", + message: "Something went wrong" + ) + let result = mapper.map(error, surfaceID: "s1") + if case .generic(let generic) = result { + #expect(generic.code == "TEST_ERROR") + #expect(generic.surfaceID == "s1") + } else { + Issue.record("Expected .generic") + } + } + + @Test func mapUnknownErrorToGeneric() { + struct CustomError: Error {} + let result = mapper.map(CustomError(), surfaceID: "s1") + if case .generic(let generic) = result { + #expect(generic.code == "PARSING_FAILED") + #expect(generic.surfaceID == "s1") + } else { + Issue.record("Expected .generic") + } + } + + @Test func extractSurfaceIDFromValidLine() { + let parser = MessageParser() + let id = parser.extractSurfaceID( + fromLine: """ + { + "createSurface": { + "surfaceId": "s1", + "catalogId": "default" + } + } + """) + #expect(id == "s1") + } + + @Test func extractSurfaceIDReturnsNilForInvalidJson() { + let parser = MessageParser() + let id = parser.extractSurfaceID(fromLine: "not valid json") + #expect(id == nil) + } +} diff --git a/swift/core/Tests/A2UICoreTests/MessageProcessorTests.swift b/swift/core/Tests/A2UICoreTests/MessageProcessorTests.swift new file mode 100644 index 0000000000..d98a8e0ba1 --- /dev/null +++ b/swift/core/Tests/A2UICoreTests/MessageProcessorTests.swift @@ -0,0 +1,485 @@ +// Copyright 2026 Google LLC +// +// Licensed 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 +// +// https://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 A2UICore +import A2UIJSON +import Foundation +import JSONSchema +import OrderedJSON +import Testing + +struct MessageParserTests { + + @Test func parseValidCreateSurface() throws { + let parser = MessageParser() + let json = """ + { + "version": "v0.9.1", + "createSurface": { + "surfaceId": "s1", + "catalogId": "default" + } + } + """ + let msg = try parser.parse(jsonString: json) + if case .createSurface(let create) = msg { + #expect(create.surfaceID == "s1") + #expect(create.catalogID == "default") + } else { + Issue.record("Expected .createSurface") + } + } + + @Test func parseValidUpdateComponents() throws { + let parser = MessageParser() + let json = """ + { + "version": "v0.9.1", + "updateComponents": { + "surfaceId": "s1", + "components": [] + } + } + """ + let msg = try parser.parse(jsonString: json) + if case .updateComponents(let update) = msg { + #expect(update.surfaceID == "s1") + } else { + Issue.record("Expected .updateComponents") + } + } + + @Test func parseInvalidJsonThrows() throws { + let parser = MessageParser() + #expect(throws: DecodingError.self) { + try parser.parse(jsonString: "not valid json") + } + } + + @Test func parseEmptyObjectThrows() throws { + let parser = MessageParser() + #expect(throws: DecodingError.self) { + try parser.parse(jsonString: "{}") + } + } + + @Test func decodeFromData() throws { + let parser = MessageParser() + let json = try #require( + """ + { + "version": "v0.9.1", + "deleteSurface": { + "surfaceId": "s1" + } + } + """.data(using: .utf8) + ) + let msg = try parser.decode(jsonData: json) + if case .deleteSurface(let delete) = msg { + #expect(delete.surfaceID == "s1") + } else { + Issue.record("Expected .deleteSurface") + } + } +} + +@MainActor +struct MessageProcessorTests { + + // MARK: - Setup + + private func makeProcessor() throws -> (MessageProcessor, TestProcessorActionHandler) { + let handler = TestProcessorActionHandler() + let catalog = try makeMessageProcessorTestCatalog() + let processor = MessageProcessor( + catalogs: ["default": catalog], + actionHandler: handler + ) + return (processor, handler) + } + + // MARK: - Create Surface + + @Test func processCreateSurfaceCreatesSurface() throws { + let (processor, _) = try makeProcessor() + try processor.process( + line: """ + { + "version": "v0.9.1", + "createSurface": { + "surfaceId": "s1", + "catalogId": "default" + } + } + """) + #expect(processor.getSurface("s1") != nil) + } + + @Test func processCreateSurfaceWithUnknownCatalogThrows() throws { + let (processor, handler) = try makeProcessor() + #expect(throws: GenericError.self) { + try processor.process( + line: """ + { + "version": "v0.9.1", + "createSurface": { + "surfaceId": "s1", + "catalogId": "unknown" + } + } + """) + } + #expect(handler.capturedErrors.count == 1) + } + + @Test func processCreateSurfaceWithTheme() throws { + let (processor, _) = try makeProcessor() + try processor.process( + line: """ + { + "version": "v0.9.1", + "createSurface": { + "surfaceId": "s1", + "catalogId": "default", + "theme": { + "color": "blue" + } + } + } + """) + #expect(processor.getSurface("s1") != nil) + } + + // MARK: - Update Components + + @Test func processUpdateComponents() throws { + let (processor, _) = try makeProcessor() + try processor.process( + line: """ + { + "version": "v0.9.1", + "createSurface": { + "surfaceId": "s1", + "catalogId": "default" + } + } + """) + try processor.process( + line: """ + { + "version": "v0.9.1", + "updateComponents": { + "surfaceId": "s1", + "components": [ + { + "id": "root", + "component": "text", + "text": "Hello" + } + ] + } + } + """) + let vm = processor.getSurface("s1") + let components = vm?.componentsModel.snapshot() + #expect(components?["root"] != nil) + } + + @Test func processUpdateComponentsForMissingSurfaceThrows() throws { + let (processor, handler) = try makeProcessor() + #expect(throws: GenericError.self) { + try processor.process( + line: """ + { + "version": "v0.9.1", + "updateComponents": { + "surfaceId": "missing", + "components": [] + } + } + """) + } + #expect(handler.capturedErrors.count == 1) + } + + // MARK: - Update Data Model + + @Test func processUpdateDataModel() throws { + let (processor, _) = try makeProcessor() + try processor.process( + line: """ + { + "version": "v0.9.1", + "createSurface": { + "surfaceId": "s1", + "catalogId": "default" + } + } + """) + try processor.process( + line: """ + { + "version": "v0.9.1", + "updateDataModel": { + "surfaceId": "s1", + "path": "/user/name", + "value": "Alice" + } + } + """) + let vm = processor.getSurface("s1") + let data = vm?.dataModel.snapshot() + #expect(data?["user/name"]?.stringValue == "Alice") + } + + // MARK: - Delete Surface + + @Test func processDeleteSurface() throws { + let (processor, _) = try makeProcessor() + try processor.process( + line: """ + { + "version": "v0.9.1", + "createSurface": { + "surfaceId": "s1", + "catalogId": "default" + } + } + """) + try processor.process( + line: """ + { + "version": "v0.9.1", + "deleteSurface": { + "surfaceId": "s1" + } + } + """) + #expect(processor.getSurface("s1") == nil) + } + + @Test func processDeleteSurfaceForMissingSurfaceThrows() throws { + let (processor, handler) = try makeProcessor() + #expect(throws: GenericError.self) { + try processor.process( + line: """ + { + "version": "v0.9.1", + "deleteSurface": { + "surfaceId": "missing" + } + } + """) + } + #expect(handler.capturedErrors.count == 1) + } + + // MARK: - Error Handling + + @Test func processInvalidJsonRoutesError() throws { + let (processor, handler) = try makeProcessor() + #expect(throws: DecodingError.self) { + try processor.process(line: "not valid json") + } + #expect(handler.capturedErrors.count == 1) + } + + // MARK: - Surface Management + + @Test func groupAllSurfacesReturnsAllActiveSurfaces() throws { + let (processor, _) = try makeProcessor() + try processor.process( + line: """ + { + "version": "v0.9.1", + "createSurface": { + "surfaceId": "s1", + "catalogId": "default" + } + } + """) + try processor.process( + line: """ + { + "version": "v0.9.1", + "createSurface": { + "surfaceId": "s2", + "catalogId": "default" + } + } + """) + let surfaces = processor.surfaceGroupModel.allSurfaces() + #expect(surfaces.count == 2) + #expect(surfaces["s1"] != nil) + #expect(surfaces["s2"] != nil) + } + + @Test func groupSurfaceReturnsNilForUnknownID() throws { + let (processor, _) = try makeProcessor() + #expect(processor.getSurface("unknown") == nil) + } + + // MARK: - sendDataModel + + @Test func processCreateSurfaceWithSendDataModelSetsFlag() throws { + let (processor, _) = try makeProcessor() + try processor.process( + line: """ + { + "version": "v0.9.1", + "createSurface": { + "surfaceId": "s1", + "catalogId": "default", + "sendDataModel": true + } + } + """) + let dataModel = processor.getClientDataModel() + #expect(dataModel != nil) + } + + @Test func processCreateSurfaceWithoutSendDataModelDoesNotSetFlag() throws { + let (processor, _) = try makeProcessor() + try processor.process( + line: """ + { + "version": "v0.9.1", + "createSurface": { + "surfaceId": "s1", + "catalogId": "default" + } + } + """) + #expect(processor.getClientDataModel() == nil) + } + + // MARK: - getClientCapabilities + + @Test func getClientCapabilitiesReturnsSupportedCatalogIds() throws { + let (processor, _) = try makeProcessor() + let caps = processor.getClientCapabilities() + #expect(caps["v0.9.1"]?["supportedCatalogIds"]?.arrayValue?.first?.stringValue == "default") + } + + @Test func getClientCapabilitiesIncludesInlineCatalogs() throws { + let (processor, _) = try makeProcessor() + let caps = processor.getClientCapabilities( + options: MessageProcessor.CapabilitiesOptions(includeInlineCatalogs: true) + ) + let inlineCatalogs = caps["v0.9.1"]?["inlineCatalogs"]?.arrayValue + #expect(inlineCatalogs?.count == 1) + let firstCatalog = inlineCatalogs?.first + #expect(firstCatalog?["catalogId"]?.stringValue == "default") + #expect(firstCatalog?["components"]?["text"] != nil) + } + + @Test func getClientCapabilitiesTransformsRefDescriptions() throws { + let customSchema = try Schema( + instance: """ + { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "REF:common_types.json#/$defs/DynamicString|The title" + } + } + } + """ + ) + let catalog = Catalog( + id: "cat-ref", + components: [ComponentAPI(name: "Custom", schema: customSchema)] + ) + let processor = MessageProcessor(catalogs: [catalog]) + let caps = processor.getClientCapabilities( + options: MessageProcessor.CapabilitiesOptions(includeInlineCatalogs: true) + ) + + let inlineCatalog = caps["v0.9.1"]?["inlineCatalogs"]?.arrayValue?.first + let customComponent = inlineCatalog?["components"]?["Custom"] + let titleProp = customComponent?["allOf/1/properties/title"] + + #expect(titleProp?["$ref"]?.stringValue == "common_types.json#/$defs/DynamicString") + #expect(titleProp?["description"]?.stringValue == "The title") + #expect(titleProp?["type"] == nil) + } + + @Test func getClientCapabilitiesGeneratesFunctionsAndThemeSchema() throws { + let funcSchema = try Schema( + instance: """ + { + "type": "object", + "description": "Adds two numbers", + "properties": { + "a": { "type": "number" }, + "b": { "type": "number" } + } + } + """ + ) + + let addFunc = TestAddFunction(schema: funcSchema) + + let themeSchema = try Schema( + instance: """ + { + "type": "object", + "properties": { + "primaryColor": { + "type": "string", + "description": "REF:common_types.json#/$defs/Color|The main color" + } + } + } + """ + ) + + let textSchema = try Schema(instance: "{\"type\": \"object\"}") + let catalog = Catalog( + id: "cat-full", + components: [ComponentAPI(name: "Button", schema: textSchema)], + functions: [addFunc], + themeSchema: themeSchema + ) + let processor = MessageProcessor(catalogs: [catalog]) + let caps = processor.getClientCapabilities( + options: MessageProcessor.CapabilitiesOptions(includeInlineCatalogs: true) + ) + + let inlineCatalog = caps["v0.9.1"]?["inlineCatalogs"]?.arrayValue?.first + #expect(inlineCatalog?["catalogId"]?.stringValue == "cat-full") + + let functions = inlineCatalog?["functions"]?.arrayValue + #expect(functions?.count == 1) + #expect(functions?.first?["name"]?.stringValue == "add") + #expect(functions?.first?["returnType"]?.stringValue == "number") + #expect(functions?.first?["description"]?.stringValue == "Adds two numbers") + + let theme = inlineCatalog?["theme"] + #expect(theme?["primaryColor"]?["$ref"]?.stringValue == "common_types.json#/$defs/Color") + #expect(theme?["primaryColor"]?["description"]?.stringValue == "The main color") + } +} + +private struct TestAddFunction: FunctionImplementation { + let api: FunctionAPI + init(schema: Schema) { + self.api = FunctionAPI(name: "add", returnType: .number, schema: schema) + } + func evaluate(arguments: [String: JSONValue]) throws -> JSONValue { + .null + } +} diff --git a/swift/core/Tests/A2UICoreTests/ServerToClientMessageTests.swift b/swift/core/Tests/A2UICoreTests/ServerToClientMessageTests.swift index c8cc5abb54..4acc3715a2 100644 --- a/swift/core/Tests/A2UICoreTests/ServerToClientMessageTests.swift +++ b/swift/core/Tests/A2UICoreTests/ServerToClientMessageTests.swift @@ -22,7 +22,8 @@ struct ServerToClientMessageTests { // MARK: - Decoding @Test func decodeCreateSurface() throws { - let json = try #require(""" + let json = try #require( + """ { "version": "v0.9.1", "createSurface": { @@ -44,7 +45,8 @@ struct ServerToClientMessageTests { } @Test func decodeCreateSurfaceWithSendDataModel() throws { - let json = try #require(""" + let json = try #require( + """ { "version": "v0.9.1", "createSurface": { @@ -63,7 +65,8 @@ struct ServerToClientMessageTests { } @Test func decodeUpdateComponents() throws { - let json = try #require(""" + let json = try #require( + """ { "version": "v0.9.1", "updateComponents": { @@ -87,7 +90,8 @@ struct ServerToClientMessageTests { } @Test func decodeUpdateDataModel() throws { - let json = try #require(""" + let json = try #require( + """ { "version": "v0.9.1", "updateDataModel": { @@ -110,7 +114,8 @@ struct ServerToClientMessageTests { } @Test func decodeUpdateDataModelDefaultsToRootPath() throws { - let json = try #require(""" + let json = try #require( + """ { "version": "v0.9.1", "updateDataModel": { @@ -128,7 +133,8 @@ struct ServerToClientMessageTests { } @Test func decodeDeleteSurface() throws { - let json = try #require(""" + let json = try #require( + """ { "version": "v0.9.1", "deleteSurface": { @@ -165,7 +171,8 @@ struct ServerToClientMessageTests { } @Test func decodeRejectsUnsupportedVersion() throws { - let json = try #require(""" + let json = try #require( + """ { "version": "v2.0", "createSurface": {"surfaceId": "s1", "catalogId": "default"} @@ -177,7 +184,8 @@ struct ServerToClientMessageTests { } @Test func decodeAcceptsVersion09() throws { - let json = try #require(""" + let json = try #require( + """ { "version": "v0.9", "createSurface": {"surfaceId": "s1", "catalogId": "default"} diff --git a/swift/core/Tests/A2UICoreTests/SurfaceGroupModelTests.swift b/swift/core/Tests/A2UICoreTests/SurfaceGroupModelTests.swift new file mode 100644 index 0000000000..33dad0cb5f --- /dev/null +++ b/swift/core/Tests/A2UICoreTests/SurfaceGroupModelTests.swift @@ -0,0 +1,75 @@ +// Copyright 2026 Google LLC +// +// Licensed 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 +// +// https://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 A2UICore +import A2UIJSON +import Foundation +import JSONSchema +import OrderedJSON +import Testing + +@MainActor +struct SurfaceGroupModelTests { + + @Test func addSurfacePublishesToSurfacesMap() throws { + let group = SurfaceGroupModel() + let catalog = try makeTestCatalog() + let vm = SurfaceViewModel(surfaceID: "s1", catalog: catalog) + group.addSurface(vm) + #expect(group.surface(id: "s1") != nil) + #expect(group.allSurfaces().count == 1) + } + + @Test func addDuplicateSurfaceIsIgnored() throws { + let group = SurfaceGroupModel() + let catalog = try makeTestCatalog() + let vm1 = SurfaceViewModel(surfaceID: "s1", catalog: catalog) + let vm2 = SurfaceViewModel(surfaceID: "s1", catalog: catalog) + group.addSurface(vm1) + group.addSurface(vm2) + #expect(group.allSurfaces().count == 1) + } + + @Test func removeSurfaceRemovesFromGroup() throws { + let group = SurfaceGroupModel() + let catalog = try makeTestCatalog() + let vm = SurfaceViewModel(surfaceID: "s1", catalog: catalog) + group.addSurface(vm) + group.removeSurface(id: "s1") + #expect(group.surface(id: "s1") == nil) + #expect(group.allSurfaces().isEmpty) + } + + @Test func getClientDataModelReturnsNilWhenNoFlagSet() throws { + let group = SurfaceGroupModel() + let catalog = try makeTestCatalog() + let vm = SurfaceViewModel(surfaceID: "s1", catalog: catalog) + group.addSurface(vm) + #expect(group.getClientDataModel() == nil) + } + + @Test func getClientDataModelAggregatesFlaggedSurfaces() throws { + let group = SurfaceGroupModel() + let catalog = try makeTestCatalog() + let vm1 = SurfaceViewModel(surfaceID: "s1", catalog: catalog) + let vm2 = SurfaceViewModel(surfaceID: "s2", catalog: catalog) + vm2.dataModel.set("/foo", value: "hello") + group.addSurface(vm1) + group.addSurface(vm2) + group.setSendDataModel(surfaceID: "s2", enabled: true) + let dataModel = try #require(group.getClientDataModel()) + let s2Data = try #require(dataModel["s2"]) + #expect(s2Data["foo"]?.stringValue == "hello") + } +} diff --git a/swift/core/Tests/A2UICoreTests/SurfaceViewModelTests.swift b/swift/core/Tests/A2UICoreTests/SurfaceViewModelTests.swift index b1a6606f53..2ff77268f8 100644 --- a/swift/core/Tests/A2UICoreTests/SurfaceViewModelTests.swift +++ b/swift/core/Tests/A2UICoreTests/SurfaceViewModelTests.swift @@ -55,19 +55,19 @@ struct TestSurfaceTheme: SurfaceTheme { func makeTestCatalog() throws -> Catalog { let buttonSchema = try Schema( instance: """ - { - "type": "object", - "properties": { - "id": { "type": "string" }, - "component": { "type": "string" }, - "label": { "$ref": "https://a2ui.org/schemas/v0_9_1/common.json#/$defs/DynamicString" }, - "enabled": { "$ref": "https://a2ui.org/schemas/v0_9_1/common.json#/$defs/DynamicBoolean" }, - "onClick": { "$ref": "https://a2ui.org/schemas/v0_9_1/common.json#/$defs/Action" }, - "children": { "$ref": "https://a2ui.org/schemas/v0_9_1/common.json#/$defs/ChildList" } - }, - "required": ["id", "component"] - } - """, + { + "type": "object", + "properties": { + "id": { "type": "string" }, + "component": { "type": "string" }, + "label": { "$ref": "https://a2ui.org/schemas/v0_9_1/common.json#/$defs/DynamicString" }, + "enabled": { "$ref": "https://a2ui.org/schemas/v0_9_1/common.json#/$defs/DynamicBoolean" }, + "onClick": { "$ref": "https://a2ui.org/schemas/v0_9_1/common.json#/$defs/Action" }, + "children": { "$ref": "https://a2ui.org/schemas/v0_9_1/common.json#/$defs/ChildList" } + }, + "required": ["id", "component"] + } + """, remoteSchemas: A2UICommonSchema.allSchemas ) @@ -92,6 +92,7 @@ final class TestActionHandler: ActionHandling, @unchecked Sendable { } } +@MainActor struct SurfaceViewModelTests { // MARK: - Setup Helper @@ -116,7 +117,7 @@ struct SurfaceViewModelTests { processor.updateComponents( surfaceID: surface.surfaceID, components: [ - ["id": "root", "component": "button", "label": "Click Me"], + ["id": "root", "component": "button", "label": "Click Me"] ] ) #expect(surface.componentsModel.get("root") != nil) @@ -128,7 +129,7 @@ struct SurfaceViewModelTests { processor.updateComponents( surfaceID: "test-surface", components: [ - ["id": "root"], + ["id": "root"] ] ) #expect(handler.capturedErrors.count == 1) @@ -142,7 +143,7 @@ struct SurfaceViewModelTests { processor.updateComponents( surfaceID: "test-surface", components: [ - ["component": "button"], + ["component": "button"] ] ) #expect(handler.capturedErrors.count == 1) @@ -156,7 +157,7 @@ struct SurfaceViewModelTests { processor.updateComponents( surfaceID: "test-surface", components: [ - ["id": "root", "component": "unknown_type"], + ["id": "root", "component": "unknown_type"] ] ) #expect(handler.capturedErrors.count == 1) @@ -167,7 +168,7 @@ struct SurfaceViewModelTests { processor.updateComponents( surfaceID: "test-surface", components: [ - ["id": "root", "component": "button", "label": "Old"], + ["id": "root", "component": "button", "label": "Old"] ] ) #expect(surface.componentsModel.get("root")?.type == "button") @@ -176,7 +177,7 @@ struct SurfaceViewModelTests { processor.updateComponents( surfaceID: "test-surface", components: [ - ["id": "root", "component": "text"], + ["id": "root", "component": "text"] ] ) // The component should not be stored since "text" isn't in the catalog @@ -215,7 +216,7 @@ struct SurfaceViewModelTests { processor.updateComponents( surfaceID: surface.surfaceID, components: [ - ["id": "root", "component": "button", "label": "Hello"], + ["id": "root", "component": "button", "label": "Hello"] ] ) let root = surface.componentsModel.get("root") @@ -228,7 +229,7 @@ struct SurfaceViewModelTests { processor.updateComponents( surfaceID: surface.surfaceID, components: [ - ["id": "root", "component": "button", "label": ["path": "/user/name"]], + ["id": "root", "component": "button", "label": ["path": "/user/name"]] ] ) let data = surface.dataModel.snapshot() @@ -242,7 +243,7 @@ struct SurfaceViewModelTests { processor.updateComponents( surfaceID: surface.surfaceID, components: [ - ["id": "root", "component": "button", "enabled": true], + ["id": "root", "component": "button", "enabled": true] ] ) let root = surface.componentsModel.get("root") @@ -263,9 +264,9 @@ struct SurfaceViewModelTests { "event": [ "name": "click", "context": ["userId": "123"], - ], + ] ], - ], + ] ] ) let root = surface.componentsModel.get("root") @@ -286,9 +287,9 @@ struct SurfaceViewModelTests { "functionCall": [ "call": "submit", "args": ["formId": "contact"], - ], + ] ], - ], + ] ] ) let root = surface.componentsModel.get("root") @@ -359,16 +360,16 @@ struct SurfaceViewModelTests { // property (no DataBinding wrapping). let schema = try Schema( instance: """ - { - "type": "object", - "properties": { - "id": { "type": "string" }, - "component": { "type": "string" }, - "items": { "$ref": "https://a2ui.org/schemas/v0_9_1/common.json#/$defs/DynamicStringList" } - }, - "required": ["id", "component"] - } - """, + { + "type": "object", + "properties": { + "id": { "type": "string" }, + "component": { "type": "string" }, + "items": { "$ref": "https://a2ui.org/schemas/v0_9_1/common.json#/$defs/DynamicStringList" } + }, + "required": ["id", "component"] + } + """, remoteSchemas: A2UICommonSchema.allSchemas ) @@ -389,7 +390,7 @@ struct SurfaceViewModelTests { processor.updateComponents( surfaceID: surface.surfaceID, components: [ - ["id": "root", "component": "custom", "items": ["a", "b", "c"]], + ["id": "root", "component": "custom", "items": ["a", "b", "c"]] ] ) @@ -450,7 +451,7 @@ struct SurfaceViewModelTests { "componentId": "card", "path": "/items", ], - ], + ] ] ) @@ -572,105 +573,3 @@ struct DataModelTests { #expect(model.get("/name")?.stringValue == "Bob") } } - -// MARK: - MessageProcessor Tests - -struct MessageProcessorTests { - - // MARK: - Setup Helper - - private func makeProcessor() throws -> (MessageProcessor, TestActionHandler) { - let handler = TestActionHandler() - let catalog = try makeTestCatalog() - let processor = MessageProcessor( - catalogs: [catalog], - actionHandler: handler - ) - return (processor, handler) - } - - // MARK: - Surface Creation - - @Test func createSurfaceWithCatalogID() throws { - let (processor, _) = try makeProcessor() - let surface = processor.createSurface(surfaceID: "s1", catalogID: "test-catalog") - #expect(surface != nil) - #expect(surface?.surfaceID == "s1") - } - - @Test func createSurfaceReturnsNilForUnknownCatalog() throws { - let (processor, _) = try makeProcessor() - let surface = processor.createSurface(surfaceID: "s1", catalogID: "unknown") - #expect(surface == nil) - } - - @Test func getSurfaceReturnsCreatedSurface() throws { - let (processor, _) = try makeProcessor() - _ = processor.createSurface(surfaceID: "s1", catalogID: "test-catalog") - #expect(processor.getSurface("s1") != nil) - } - - @Test func deleteSurfaceRemovesIt() throws { - let (processor, _) = try makeProcessor() - _ = processor.createSurface(surfaceID: "s1", catalogID: "test-catalog") - processor.deleteSurface("s1") - #expect(processor.getSurface("s1") == nil) - } - - // MARK: - Message Processing - - @Test func processCreateSurfaceMessage() throws { - let (processor, _) = try makeProcessor() - let msg = CreateSurfaceMessage(surfaceID: "s1", catalogID: "test-catalog") - processor.processMessage(.createSurface(msg)) - #expect(processor.getSurface("s1") != nil) - } - - @Test func processDeleteSurfaceMessage() throws { - let (processor, _) = try makeProcessor() - let create = CreateSurfaceMessage(surfaceID: "s1", catalogID: "test-catalog") - processor.processMessage(.createSurface(create)) - let delete = DeleteSurfaceMessage(surfaceID: "s1") - processor.processMessage(.deleteSurface(delete)) - #expect(processor.getSurface("s1") == nil) - } - - @Test func processUpdateComponentsMessage() throws { - let (processor, handler) = try makeProcessor() - let create = CreateSurfaceMessage(surfaceID: "s1", catalogID: "test-catalog") - processor.processMessage(.createSurface(create)) - - let update = UpdateComponentsMessage( - surfaceID: "s1", - components: [["id": "root", "component": "button", "label": "Hi"]] - ) - processor.processMessage(.updateComponents(update)) - - let surface = processor.getSurface("s1") - #expect(surface?.componentsModel.get("root") != nil) - #expect(handler.capturedErrors.isEmpty) - } - - @Test func processUpdateDataModelMessage() throws { - let (processor, _) = try makeProcessor() - let create = CreateSurfaceMessage(surfaceID: "s1", catalogID: "test-catalog") - processor.processMessage(.createSurface(create)) - - let update = UpdateDataModelMessage(surfaceID: "s1", path: "/foo", value: "bar") - processor.processMessage(.updateDataModel(update)) - - let surface = processor.getSurface("s1") - #expect(surface?.dataModel.get("/foo")?.stringValue == "bar") - } - - @Test func processMultipleMessages() throws { - let (processor, _) = try makeProcessor() - let messages: [ServerToClientMessage] = [ - .createSurface(CreateSurfaceMessage(surfaceID: "s1", catalogID: "test-catalog")), - .updateDataModel(UpdateDataModelMessage(surfaceID: "s1", path: "/x", value: 42)), - .deleteSurface(DeleteSurfaceMessage(surfaceID: "s1")), - ] - processor.processMessages(messages) - #expect(processor.getSurface("s1") == nil) - } -} diff --git a/swift/core/Tests/A2UICoreTests/TestHelpers.swift b/swift/core/Tests/A2UICoreTests/TestHelpers.swift new file mode 100644 index 0000000000..8dc3acf902 --- /dev/null +++ b/swift/core/Tests/A2UICoreTests/TestHelpers.swift @@ -0,0 +1,58 @@ +// Copyright 2026 Google LLC +// +// Licensed 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 +// +// https://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 A2UICore +import A2UIJSON +import Foundation +import JSONSchema +import OrderedJSON +import Testing + +/// Helper to create a catalog with a simple text component schema for testing. +func makeMessageProcessorTestCatalog() throws -> Catalog { + let textSchema = try Schema( + instance: """ + { + "type": "object", + "properties": { + "id": { "type": "string" }, + "component": { "type": "string" }, + "text": { "$ref": "https://a2ui.org/schemas/v0_9_1/common.json#/$defs/DynamicString" } + }, + "required": ["id", "component"] + } + """, + remoteSchemas: A2UICommonSchema.allSchemas + ) + return Catalog( + id: "default", + components: [ComponentAPI(name: "text", schema: textSchema)] + ) +} + +/// A test action handler that captures actions and errors. +final class TestProcessorActionHandler: ActionHandling, + @unchecked Sendable +{ + var capturedActions: [ResolvedAction] = [] + var capturedErrors: [ClientServerError] = [] + + func handle(action: ResolvedAction, from surfaceID: String) { + capturedActions.append(action) + } + + func handle(error: ClientServerError, from surfaceID: String) { + capturedErrors.append(error) + } +} diff --git a/swift/swiftui/Sources/A2UISwiftUI/A2UIThemeKey.swift b/swift/swiftui/Sources/A2UISwiftUI/A2UIThemeKey.swift new file mode 100644 index 0000000000..47db099d65 --- /dev/null +++ b/swift/swiftui/Sources/A2UISwiftUI/A2UIThemeKey.swift @@ -0,0 +1,30 @@ +// Copyright 2026 Google LLC +// +// Licensed 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 +// +// https://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 A2UICore +import SwiftUI + +/// Environment key for propagating the active `SurfaceTheme` through +/// the SwiftUI view hierarchy. +public struct A2UIThemeKey: EnvironmentKey { + public static let defaultValue: (any SurfaceTheme)? = nil +} + +extension EnvironmentValues { + /// The active A2UI surface theme, if any. + public var a2uiTheme: (any SurfaceTheme)? { + get { self[A2UIThemeKey.self] } + set { self[A2UIThemeKey.self] = newValue } + } +} diff --git a/swift/swiftui/Sources/A2UISwiftUI/A2UISwiftUI.swift b/swift/swiftui/Sources/A2UISwiftUI/CatalogView.swift similarity index 68% rename from swift/swiftui/Sources/A2UISwiftUI/A2UISwiftUI.swift rename to swift/swiftui/Sources/A2UISwiftUI/CatalogView.swift index 43a661517c..a44d3363a0 100644 --- a/swift/swiftui/Sources/A2UISwiftUI/A2UISwiftUI.swift +++ b/swift/swiftui/Sources/A2UISwiftUI/CatalogView.swift @@ -15,6 +15,10 @@ import A2UICore import SwiftUI -// A2UISwiftUI — Thin SwiftUI rendering layer for A2UI. -// This file is intentionally a placeholder; real types (Surface, CatalogView, -// DataBinding+SwiftUI, A2UIThemeKey) will be added in a future PR. +/// A protocol that all catalog views must conform to, enabling the +/// SwiftUI rendering layer to construct catalog-defined views from +/// resolved engine nodes. +public protocol CatalogView: View { + /// Initializes the catalog view with a resolved engine node. + init(node: Node) +} diff --git a/swift/swiftui/Sources/A2UISwiftUI/DataBinding+SwiftUI.swift b/swift/swiftui/Sources/A2UISwiftUI/DataBinding+SwiftUI.swift new file mode 100644 index 0000000000..d510cffaa3 --- /dev/null +++ b/swift/swiftui/Sources/A2UISwiftUI/DataBinding+SwiftUI.swift @@ -0,0 +1,35 @@ +// Copyright 2026 Google LLC +// +// Licensed 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 +// +// https://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 A2UICore +import SwiftUI + +/// Provides SwiftUI `Binding` access to `DataBinding` values. +/// +/// This extension bridges A2UI's thread-safe `DataBinding` to +/// SwiftUI's `Binding` for use in form controls and other two-way +/// bound views. +extension DataBinding { + /// A SwiftUI `Binding` backed by this `DataBinding`. + public var swiftUIBinding: Binding { + Binding( + get: { + self.get() + }, + set: { newValue in + self.set(newValue) + } + ) + } +} diff --git a/swift/swiftui/Sources/A2UISwiftUI/Surface.swift b/swift/swiftui/Sources/A2UISwiftUI/Surface.swift new file mode 100644 index 0000000000..8a26ea596f --- /dev/null +++ b/swift/swiftui/Sources/A2UISwiftUI/Surface.swift @@ -0,0 +1,43 @@ +// Copyright 2026 Google LLC +// +// Licensed 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 +// +// https://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 A2UICore +import SwiftUI + +/// The root SwiftUI view for a single A2UI surface. +/// +/// `Surface` observes a ``SurfaceViewModel`` and renders the resolved +/// component tree via a catalog-defined ``CatalogView``. The active +/// theme is propagated through the environment. +public struct Surface: View { + @ObservedObject public var viewModel: SurfaceViewModel + + private let catalogType: Catalog.Type + public let surfaceID: String + + public init(viewModel: SurfaceViewModel, catalogType: Catalog.Type) { + self.viewModel = viewModel + self.catalogType = catalogType + self.surfaceID = viewModel.surfaceID + } + + public var body: some View { + if let rootNode = viewModel.rootNode { + Catalog(node: rootNode) + .environment(\.a2uiTheme, viewModel.getActiveTheme()) + } else { + ProgressView() + } + } +} diff --git a/swift/swiftui/Tests/A2UISwiftUITests/A2UISwiftUITests.swift b/swift/swiftui/Tests/A2UISwiftUITests/A2UISwiftUITests.swift index 22325ef86b..d352f4d64a 100644 --- a/swift/swiftui/Tests/A2UISwiftUITests/A2UISwiftUITests.swift +++ b/swift/swiftui/Tests/A2UISwiftUITests/A2UISwiftUITests.swift @@ -12,11 +12,168 @@ // See the License for the specific language governing permissions and // limitations under the License. +import A2UICore +import A2UIJSON import A2UISwiftUI +import JSONSchema +import OrderedJSON +import SwiftUI import Testing -struct A2UISwiftUITests { - @Test func placeholderTest() { - #expect(Bool(true)) +// MARK: - Test Helpers + +/// A simple catalog view for testing that renders a text label. +struct TestCatalogView: CatalogView { + let node: Node + + init(node: Node) { + self.node = node + } + + var body: some View { + VStack { + Text("Type: \(node.type)") + Text("ID: \(node.id)") + } + } +} + +/// A simple theme for testing. +struct TestRenderTheme: SurfaceTheme { + let color: String +} + +// MARK: - Surface Tests + +@MainActor +struct SurfaceTests { + + @Test func surfaceInitializesWithViewModel() throws { + let catalog = try makeTestSurfaceCatalogForRendering() + let vm = SurfaceViewModel(surfaceID: "s1", catalog: catalog) + let surface = Surface(viewModel: vm, catalogType: TestCatalogView.self) + #expect(surface.surfaceID == "s1") + } + + @Test func surfaceIDMatchesViewModel() throws { + let catalog = try makeTestSurfaceCatalogForRendering() + let vm = SurfaceViewModel(surfaceID: "s1", catalog: catalog) + let a = Surface(viewModel: vm, catalogType: TestCatalogView.self) + let b = Surface(viewModel: vm, catalogType: TestCatalogView.self) + #expect(a.surfaceID == b.surfaceID) + } + + @Test func surfaceDifferentSurfaceIDs() throws { + let catalog = try makeTestSurfaceCatalogForRendering() + let vm1 = SurfaceViewModel(surfaceID: "s1", catalog: catalog) + let vm2 = SurfaceViewModel(surfaceID: "s2", catalog: catalog) + let a = Surface(viewModel: vm1, catalogType: TestCatalogView.self) + let b = Surface(viewModel: vm2, catalogType: TestCatalogView.self) + #expect(a.surfaceID != b.surfaceID) + } +} + +// MARK: - DataBinding+SwiftUI Tests + +struct DataBindingSwiftUITests { + + @Test func swiftUIBindingGetsValue() { + let box = TestBox("hello") + let binding = DataBinding( + identity: .path("/text"), + get: { box.value }, + set: { box.value = $0 } + ) + let swiftBinding = binding.swiftUIBinding + #expect(swiftBinding.wrappedValue == "hello") + } + + @Test func swiftUIBindingSetsValue() { + let box = TestBox("hello") + let binding = DataBinding( + identity: .path("/text"), + get: { box.value }, + set: { box.value = $0 } + ) + let swiftBinding = binding.swiftUIBinding + swiftBinding.wrappedValue = "world" + #expect(binding.get() == "world") } + + @Test func swiftUIBindingGetsAndSetsValue() { + let box = TestBox(42.0) + let binding = DataBinding( + identity: .path("/value"), + get: { box.value }, + set: { box.value = $0 } + ) + let swiftBinding = binding.swiftUIBinding + #expect(swiftBinding.wrappedValue == 42.0) + swiftBinding.wrappedValue = 99.0 + #expect(binding.get() == 99.0) + } +} + +// MARK: - Theme Environment Tests + +struct ThemeEnvironmentTests { + + @Test func themeKeyDefaultValueIsNil() { + #expect(A2UIThemeKey.defaultValue == nil) + } + + @Test func themeEnvironmentCanBeSet() throws { + let theme = TestRenderTheme(color: "blue") + var env = EnvironmentValues() + env.a2uiTheme = theme + #expect(env.a2uiTheme != nil) + #expect((env.a2uiTheme as? TestRenderTheme)?.color == "blue") + } + + @Test func themeEnvironmentDefaultsToNil() { + let env = EnvironmentValues() + #expect(env.a2uiTheme == nil) + } +} + +// MARK: - CatalogView Tests + +struct CatalogViewTests { + + @MainActor @Test func catalogViewInitializesWithNode() { + let node = Node(id: "test", type: "text", properties: ["label": "Hello"]) + let view = TestCatalogView(node: node) + #expect(view.node.id == "test") + #expect(view.node.type == "text") + } +} + +// MARK: - Helpers + +/// A mutable box for testing Sendable closures. +final class TestBox: @unchecked Sendable { + var value: T + init(_ value: T) { self.value = value } +} + +/// Helper function returning a catalog with a simple text schema for rendering tests. +func makeTestSurfaceCatalogForRendering() throws -> Catalog { + let textSchema = try Schema( + instance: """ + { + "type": "object", + "properties": { + "id": { "type": "string" }, + "component": { "type": "string" }, + "text": { "$ref": "https://a2ui.org/schemas/v0_9_1/common.json#/$defs/DynamicString" } + }, + "required": ["id", "component"] + } + """, + remoteSchemas: A2UICommonSchema.allSchemas + ) + return Catalog( + id: "default", + components: [ComponentAPI(name: "text", schema: textSchema)] + ) } diff --git a/uv.lock b/uv.lock index 5b0d9b30e0..3aef7a569d 100644 --- a/uv.lock +++ b/uv.lock @@ -17,8 +17,6 @@ resolution-markers = [ members = [ "a2ui-agent-sdk", "a2ui-conformance-validation", - "a2ui-contact-lookup", - "a2ui-contactcard-agentengine", "a2ui-core", "a2ui-custom-components-example", "a2ui-eval", @@ -111,50 +109,6 @@ requires-dist = [ { name = "pyyaml", specifier = ">=6.0.1" }, ] -[[package]] -name = "a2ui-contact-lookup" -version = "0.1.0" -source = { editable = "samples/community/agent/adk/gemini_enterprise/cloud_run" } -dependencies = [ - { name = "a2a-sdk", extra = ["http-server"] }, - { name = "a2ui-agent-sdk" }, - { name = "google-adk" }, - { name = "google-cloud-aiplatform" }, - { name = "google-genai" }, - { name = "jsonschema" }, - { name = "python-dotenv" }, - { name = "uvicorn" }, -] - -[package.metadata] -requires-dist = [ - { name = "a2a-sdk", extras = ["http-server"], specifier = "==0.3.25" }, - { name = "a2ui-agent-sdk", editable = "agent_sdks/python/a2ui_agent" }, - { name = "google-adk", specifier = ">=1.28.0" }, - { name = "google-cloud-aiplatform", specifier = ">=1.84" }, - { name = "google-genai", specifier = ">=1.27.0" }, - { name = "jsonschema", specifier = ">=4.0.0" }, - { name = "python-dotenv", specifier = ">=1.1.0" }, - { name = "uvicorn" }, -] - -[[package]] -name = "a2ui-contactcard-agentengine" -version = "0.1.0" -source = { virtual = "samples/community/agent/adk/gemini_enterprise/agent_engine" } -dependencies = [ - { name = "a2a-sdk" }, - { name = "a2ui-agent-sdk" }, - { name = "google-cloud-aiplatform", extra = ["adk", "agent-engines"] }, -] - -[package.metadata] -requires-dist = [ - { name = "a2a-sdk", specifier = "==0.3.25" }, - { name = "a2ui-agent-sdk", editable = "agent_sdks/python/a2ui_agent" }, - { name = "google-cloud-aiplatform", extras = ["adk", "agent-engines"], specifier = ">=1.128.0" }, -] - [[package]] name = "a2ui-core" source = { editable = "agent_sdks/python/a2ui_core" } @@ -1193,15 +1147,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, ] -[[package]] -name = "cloudpickle" -version = "3.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -1614,12 +1559,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/86/40/9bdbb60b03a332bd45acb8703da08bbc27d991d35286b62e42acc86d243a/google_api_core-2.31.0-py3-none-any.whl", hash = "sha256:ef79fb3784c71cbac89cbd03301ba0c8fb8ad2aa95d7f9204dd9628f7adf59ab", size = 173102, upload-time = "2026-06-03T14:51:26.729Z" }, ] -[package.optional-dependencies] -grpc = [ - { name = "grpcio" }, - { name = "grpcio-status" }, -] - [[package]] name = "google-auth" version = "2.55.1" @@ -1641,96 +1580,6 @@ requests = [ { name = "requests" }, ] -[[package]] -name = "google-cloud-aiplatform" -version = "1.158.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "docstring-parser" }, - { name = "google-api-core", extra = ["grpc"] }, - { name = "google-auth" }, - { name = "google-cloud-bigquery" }, - { name = "google-cloud-resource-manager" }, - { name = "google-cloud-storage" }, - { name = "google-genai" }, - { name = "packaging" }, - { name = "proto-plus" }, - { name = "protobuf" }, - { name = "pydantic" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d0/74/71440b7793068d8411f096712d6274a64a42f44bd01a11d67d8cbbd27b54/google_cloud_aiplatform-1.158.0.tar.gz", hash = "sha256:85b6bedc3823824617db1ea83e07fa07f00681d7ab63c42cdc584066a844737b", size = 11128785, upload-time = "2026-06-16T23:08:45.036Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/1c/4273f7a6eb59214a94575d34a3603b37519217af7dfc1bcd2387daed0219/google_cloud_aiplatform-1.158.0-py2.py3-none-any.whl", hash = "sha256:8ed07f866fe9a49c31f0ba9fc8049c5cd5b47ff7f833ca3d9d8ce480f871d715", size = 9343147, upload-time = "2026-06-16T23:08:41.398Z" }, -] - -[package.optional-dependencies] -adk = [ - { name = "google-adk" }, -] -agent-engines = [ - { name = "aiohttp" }, - { name = "cloudpickle" }, - { name = "google-cloud-iam" }, - { name = "google-cloud-logging" }, - { name = "google-cloud-trace" }, - { name = "opentelemetry-exporter-gcp-logging" }, - { name = "opentelemetry-exporter-gcp-trace" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-sdk" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "typing-extensions" }, -] - -[[package]] -name = "google-cloud-appengine-logging" -version = "1.10.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core", extra = ["grpc"] }, - { name = "google-auth" }, - { name = "grpcio" }, - { name = "proto-plus" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7f/b9/fcafc8d2dc68975a65cdff74807547cff9b2a7b00e738d3f5ff0bd112867/google_cloud_appengine_logging-1.10.0.tar.gz", hash = "sha256:b5563e76010a36e6adf1cc489620c29ee4fb3b986b006d237e9a061eb0f0abb7", size = 17744, upload-time = "2026-06-03T14:52:40.298Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/b3/4eeb9f59c4e7e07e1f08704b6508249eea5760878810014e636026300416/google_cloud_appengine_logging-1.10.0-py3-none-any.whl", hash = "sha256:193675caaf062c41688a3e2c744b73614db82408bc7fb060353b6878d7134492", size = 18143, upload-time = "2026-06-03T14:51:55.174Z" }, -] - -[[package]] -name = "google-cloud-audit-log" -version = "0.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "googleapis-common-protos" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/01/46/b971191224557091cc865b47d527e61da180e33b9397904bdefdae1dcacd/google_cloud_audit_log-0.6.0.tar.gz", hash = "sha256:4dd343683c0bb31187ebef3426803f13159e950fbea3fe60a864855cfed959b8", size = 44674, upload-time = "2026-06-03T14:52:48.095Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/99/27c70286bfa3503e43f845578ed5c2ab30c0cc68e525c168286f05f9a51c/google_cloud_audit_log-0.6.0-py3-none-any.whl", hash = "sha256:8c5ecbc341ad3b3daf776981f6d7fd7ab5ff5a29c5dce3172c669b570e0f6717", size = 44853, upload-time = "2026-06-03T14:52:03.775Z" }, -] - -[[package]] -name = "google-cloud-bigquery" -version = "3.42.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core", extra = ["grpc"] }, - { name = "google-auth", extra = ["pyopenssl"] }, - { name = "google-cloud-core" }, - { name = "google-resumable-media" }, - { name = "packaging" }, - { name = "python-dateutil" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/15/a2/be1cffbb1a9894ecf394ab0096b113ae9e4f73686595d906a9f3082b07c2/google_cloud_bigquery-3.42.1.tar.gz", hash = "sha256:3c6878f424fc2b21f0bb414ca7262abd008465575a7b7c44c552105278a18914", size = 515411, upload-time = "2026-06-22T23:21:05.884Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/1e/8b098aeb69ef87de5195d076ce6d82002c3ebf1920373d386ca7b6e63cfa/google_cloud_bigquery-3.42.1-py3-none-any.whl", hash = "sha256:159143c1b7248858f35549c09efea5d031c2883145623419ea7c505a50c0899a", size = 263814, upload-time = "2026-06-22T23:18:42.313Z" }, -] - [[package]] name = "google-cloud-core" version = "2.6.0" @@ -1744,61 +1593,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl", hash = "sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e", size = 29390, upload-time = "2026-05-07T08:02:34.672Z" }, ] -[[package]] -name = "google-cloud-iam" -version = "2.24.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core", extra = ["grpc"] }, - { name = "google-auth" }, - { name = "grpc-google-iam-v1" }, - { name = "grpcio" }, - { name = "proto-plus" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/13/d6c3f207c8147768f4b56406c1534bdda034be76712bd960a048412f7001/google_cloud_iam-2.24.0.tar.gz", hash = "sha256:199a2512723abec10a49d581e638ccdf5b8a9671e1d5d6ff3ffeaa727075435f", size = 559063, upload-time = "2026-06-22T23:21:31.061Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/74/31220c86789d4591aa493652656b83a428dcdd46f62b518e2118f98f76bd/google_cloud_iam-2.24.0-py3-none-any.whl", hash = "sha256:29d52032f774b4d3c01b6d8e1f4a8d029ae61c3bdba60ddac402866be11159db", size = 514860, upload-time = "2026-06-22T23:19:15.287Z" }, -] - -[[package]] -name = "google-cloud-logging" -version = "3.16.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core", extra = ["grpc"] }, - { name = "google-auth" }, - { name = "google-cloud-appengine-logging" }, - { name = "google-cloud-audit-log" }, - { name = "google-cloud-core" }, - { name = "grpc-google-iam-v1" }, - { name = "grpcio" }, - { name = "opentelemetry-api" }, - { name = "proto-plus" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9b/ba/e749846f13c8d1c6c01eb6317e8b09abc130fe67b5d72081a48d1bf96971/google_cloud_logging-3.16.0.tar.gz", hash = "sha256:08a3076b8f0f724219d6f73b2a242ef69d51e8bce226133aebe41a25f23f5400", size = 293703, upload-time = "2026-06-03T15:28:23.862Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/d5/91035dd77e0033dfb00d52b2bcad1e4f7408eb931981f86a1584301670a8/google_cloud_logging-3.16.0-py3-none-any.whl", hash = "sha256:9e5bfbdfe7b5315ece00e1703a2ea25fe42ca35e0b4750127b019f50d069b01b", size = 234188, upload-time = "2026-06-03T15:27:37.407Z" }, -] - -[[package]] -name = "google-cloud-resource-manager" -version = "1.18.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core", extra = ["grpc"] }, - { name = "google-auth" }, - { name = "grpc-google-iam-v1" }, - { name = "grpcio" }, - { name = "proto-plus" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/12/70/d467d93528905700826d26a48ad16ac39d4b36dd6efb5a86fb338efa81aa/google_cloud_resource_manager-1.18.0.tar.gz", hash = "sha256:db689f800a14c66d041196a7fbb8bb8aae8dc87f28c2929e101a5ec766b15512", size = 463674, upload-time = "2026-06-22T23:22:27.09Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/62/2dbecda08a32aa9d4e6909eba3e31731c2d4452687bb6364a8d7950aed74/google_cloud_resource_manager-1.18.0-py3-none-any.whl", hash = "sha256:69bab144acd75878ebe44b720903dc3d140cb7d3be3261eaa8bc81e48afaff33", size = 404211, upload-time = "2026-06-22T23:20:25.88Z" }, -] - [[package]] name = "google-cloud-storage" version = "3.12.0" @@ -1816,22 +1610,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1b/bd/a89eaebd2f9db5f92ddcc8e4f23c266be1dbd11058bb83451d8dd029f34c/google_cloud_storage-3.12.0-py3-none-any.whl", hash = "sha256:3880773754ddf7c27567b04e2a4d193950b6b99429f37b9097d873686e95b09c", size = 340605, upload-time = "2026-06-12T18:03:12.677Z" }, ] -[[package]] -name = "google-cloud-trace" -version = "1.20.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core", extra = ["grpc"] }, - { name = "google-auth" }, - { name = "grpcio" }, - { name = "proto-plus" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fd/63/18e4cfbd48ec1de3b39a3fba5500c12c7b38f14356b1b534db8b95063338/google_cloud_trace-1.20.0.tar.gz", hash = "sha256:f5a6f9b8da530b76c452163b83e5081c1696f1b4f67290c878b0e7370f1e910a", size = 103594, upload-time = "2026-06-22T23:22:39.56Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/fb/e071b9a2a0370c50bed53292c567093b77f5db15dea4642dbc8f18d7793f/google_cloud_trace-1.20.0-py3-none-any.whl", hash = "sha256:88527c02948f410ed62ceaac5c960597a4143e3c11ce62273143f11aa388f564", size = 108134, upload-time = "2026-06-22T23:20:40.235Z" }, -] - [[package]] name = "google-crc32c" version = "1.8.0" @@ -1912,11 +1690,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, ] -[package.optional-dependencies] -grpc = [ - { name = "grpcio" }, -] - [[package]] name = "graphviz" version = "0.21" @@ -1926,95 +1699,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42", size = 47300, upload-time = "2025-06-15T09:35:04.433Z" }, ] -[[package]] -name = "grpc-google-iam-v1" -version = "0.14.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "googleapis-common-protos", extra = ["grpc"] }, - { name = "grpcio" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/44/4f/d098419ad0bfc06c9ce440575f05aa22d8973b6c276e86ac7890093d3c37/grpc_google_iam_v1-0.14.4.tar.gz", hash = "sha256:392b3796947ed6334e61171d9ab06bf7eb357f554e5fc7556ad7aab6d0e17038", size = 23706, upload-time = "2026-04-01T01:57:49.813Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/22/c2dd50c09bf679bd38173656cd4402d2511e563b33bc88f90009cf50613c/grpc_google_iam_v1-0.14.4-py3-none-any.whl", hash = "sha256:412facc320fcbd94034b4df3d557662051d4d8adfa86e0ddb4dca70a3f739964", size = 32675, upload-time = "2026-04-01T01:57:47.69Z" }, -] - -[[package]] -name = "grpcio" -version = "1.81.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b0/b5/1ff353970a87eda4c98251e34d2dfd214abd4982dc89119c9252a2a482d2/grpcio-1.81.1.tar.gz", hash = "sha256:6fa10a767143a5e82e8eaab53918af0cd8909a57a27f8cb2288b80a613ac671b", size = 13026582, upload-time = "2026-06-11T12:46:51.673Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/d5/f2b159d8eec08be2a855ef698f5b6f7f9fdda022e4dd9e4f5d968affd678/grpcio-1.81.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:6f9a0c9c1cc15c112d1c053064fd032b64917062292c3d70aea280e02ae10b77", size = 6086868, upload-time = "2026-06-11T12:44:19.364Z" }, - { url = "https://files.pythonhosted.org/packages/80/41/9c95232b94b219ed8b14029d9cd000e0381cafba869c451dda60af84f4ba/grpcio-1.81.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:69ef28e54fc85397f91b8c19592b8ef3d81952080366914823bd8572a2958120", size = 12062291, upload-time = "2026-06-11T12:44:27.142Z" }, - { url = "https://files.pythonhosted.org/packages/83/8b/bd9284bdd665ddf877a3e8bc2930d1bcf6ebdbae7b0da5c783dc26bd6e33/grpcio-1.81.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:15641444eca4a29358107b3dceb74c1c6305c55c822fd199b458aaea4068a7fb", size = 6635242, upload-time = "2026-06-11T12:44:30.741Z" }, - { url = "https://files.pythonhosted.org/packages/60/24/78fa025517a925f1a17da71c4ef9d5f1c6f9fa65af22dfb523c5c6317a21/grpcio-1.81.1-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d4b2dddfc219f54f956ccd53cf76a1d338ffe68fc7f2849ec9c7feb9927ff692", size = 7332974, upload-time = "2026-06-11T12:44:33.72Z" }, - { url = "https://files.pythonhosted.org/packages/f7/11/402295b388dd35861007f8a26a37c2e2f284212d57bdf407c31f36043746/grpcio-1.81.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ca1cc11d82677b9662082e5478b7528e2b7db7beaa6bdff42bd62789d81be399", size = 6836597, upload-time = "2026-06-11T12:44:36.108Z" }, - { url = "https://files.pythonhosted.org/packages/4d/71/37b10fd4fd579ffade6e695c14e9df5e8cba9e2365b81c131da438b67c34/grpcio-1.81.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa2ba7d2ad6df4d80127cea65e5b8d5e2c3adbf153ff4804452836328aca7c54", size = 7440660, upload-time = "2026-06-11T12:44:38.664Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d5/40203f828abc83d458b634666df6df13778032f178c03845ad5a93682388/grpcio-1.81.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:592b5fee597faa91cce2dd294dd7d9a1c83d76c4dbf877e33ec1adb866b2fbed", size = 8443171, upload-time = "2026-06-11T12:44:41.678Z" }, - { url = "https://files.pythonhosted.org/packages/eb/2c/0ed82ea35b5ec595e10444940c1db8c0e0ef57aa46bc8797d5ff838a219e/grpcio-1.81.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62481553b1793a27e9b9c3cf9e5bd483ef045ca72462592074b46d42b0c4d9b9", size = 7868905, upload-time = "2026-06-11T12:44:44.854Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1f/dcbdc1a68a07cc2b631c3098953794f17d75f93426a019240b90ce5423d6/grpcio-1.81.1-cp310-cp310-win32.whl", hash = "sha256:bb693b1e3d9a2f3fd228e2110daf4b5aeedb36761ca1e4282f74725f6d89f611", size = 4202215, upload-time = "2026-06-11T12:44:47.165Z" }, - { url = "https://files.pythonhosted.org/packages/75/a1/d7ab9f1f42efcb7d9e6111d38be6b367737a72ea2c534e1f55c81e1b6436/grpcio-1.81.1-cp310-cp310-win_amd64.whl", hash = "sha256:88268ca418cacea64cecb0d1d600d3c6b3a8038fcba02e1e205178c5b1f47661", size = 4936582, upload-time = "2026-06-11T12:44:49.479Z" }, - { url = "https://files.pythonhosted.org/packages/52/ea/1c2fa386b718ff493225e61cfc052ef400b4d6ffc54cbe261026432624b5/grpcio-1.81.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:d71d30f2d92f67d944631c523713934fee37292469e182ebcd2c1dd8a64ce53f", size = 6093112, upload-time = "2026-06-11T12:44:52.131Z" }, - { url = "https://files.pythonhosted.org/packages/2b/18/acf45fa8bd1bc5d7b0c2fd3dc4c209379fbd5bb396b440b68a83342226b7/grpcio-1.81.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b137f4bf3ada9dc44d411478decc6ff09a79ed30b306cd2abaa98408c3588137", size = 12074277, upload-time = "2026-06-11T12:44:55.354Z" }, - { url = "https://files.pythonhosted.org/packages/48/d7/ee86a60699b7db039f772a2c4a7e4facc7138984ff42c0130933a0063884/grpcio-1.81.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a3acb384427816dd5d470f47e62137b87f74da694faa8a50147012cf40df276a", size = 6640348, upload-time = "2026-06-11T12:44:59.223Z" }, - { url = "https://files.pythonhosted.org/packages/26/ee/d2de5e47378ffc207d476c230fea3be4d2601edbce9995f4fe45535d4896/grpcio-1.81.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f9a0ebbe45c29b5e5866593c12b78bd9035f0f0f0d4bc8361680cd580d99db49", size = 7331842, upload-time = "2026-06-11T12:45:02.001Z" }, - { url = "https://files.pythonhosted.org/packages/23/d6/abeda5c2b896a0b341584fe5ac411bbf72e197a9a374c355fb90965e08d2/grpcio-1.81.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a37165cc80b1a368384b383e63a4c38116a10467ae44c904d2d7468c4470ec2", size = 6842229, upload-time = "2026-06-11T12:45:04.76Z" }, - { url = "https://files.pythonhosted.org/packages/10/1c/1f0da7d590b4aeee006826ba568d0e419ca14b23e18f901a3da3e9fba613/grpcio-1.81.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6282caffb41ec326d4cb67ca9cf53b739d1b2f975a2acb498c7418e9f7d9a416", size = 7446096, upload-time = "2026-06-11T12:45:07.499Z" }, - { url = "https://files.pythonhosted.org/packages/6a/81/5c505d508f7c887aa7982d21443a4126597c80d34b0bcf40f9cec576d7f3/grpcio-1.81.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a35009284d0d3d5c2c9601c164a911b8b4331608d98a9a66d47d97bb2f522b70", size = 8445238, upload-time = "2026-06-11T12:45:10.243Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b2/524847365122ee509ca17bcc4e092198b700e94af7bfd5bb5e6dd9f3ee66/grpcio-1.81.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1b22c80559854b789a01fd89e8929b3798a156c0829b5282a8939f33ad4115ad", size = 7873989, upload-time = "2026-06-11T12:45:13.102Z" }, - { url = "https://files.pythonhosted.org/packages/18/fa/07c037c50b006909d1d13a5848774f8aa7b242f70dc03a035c64eea0e6db/grpcio-1.81.1-cp311-cp311-win32.whl", hash = "sha256:428bec0161b48d8cf583c068591bc0016d0d9cfff52462b72b3884861ea768c5", size = 4202223, upload-time = "2026-06-11T12:45:16.166Z" }, - { url = "https://files.pythonhosted.org/packages/41/ed/6bff15376920942fac6b95b9802752b837437172c9e8fc2d3170546b89cc/grpcio-1.81.1-cp311-cp311-win_amd64.whl", hash = "sha256:30e825f6848d9f18bba350ed6c75c1b02a0b5184474a31db9a32b1fa66fd8c79", size = 4941303, upload-time = "2026-06-11T12:45:18.724Z" }, - { url = "https://files.pythonhosted.org/packages/85/07/9a979c81738863a738dc23d65177056e71fbb2db817740ed870b33434e7a/grpcio-1.81.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:8b39472beafc0bdcafc4c8c73ad082ebfdb449d566897a61e7acb4fa88089115", size = 6053264, upload-time = "2026-06-11T12:45:21.017Z" }, - { url = "https://files.pythonhosted.org/packages/75/95/539706ca0d3bd40dbad583dc56fd883da941f37556b629132da5762781b9/grpcio-1.81.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:12b7524c88d4026d3dcb7b0ebe16b6714f3b4af402ddd0f0639ab064a00c87c3", size = 12052560, upload-time = "2026-06-11T12:45:23.652Z" }, - { url = "https://files.pythonhosted.org/packages/e0/44/f257b7e0bd69c93b06c6cb8ac8d1b901ccb42bedabd83c1a4c77a71f8810/grpcio-1.81.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1e123f9b37edb8375fd74130d1f69c944bbf0a7b06761ae7211154b8759e94d2", size = 6595983, upload-time = "2026-06-11T12:45:26.963Z" }, - { url = "https://files.pythonhosted.org/packages/b9/f3/19782aa04c960968bef8c5539329d8e3bbc3364e2e46d19eb5e5cc5e43b7/grpcio-1.81.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2c2e2ae6867c2966b8daccc836d54a13218e0007e9a490aeb81dd05be64d22d7", size = 7303455, upload-time = "2026-06-11T12:45:29.707Z" }, - { url = "https://files.pythonhosted.org/packages/eb/8c/dea020b6d91508cd84463917a63149ec196ee7db505d032ae43fcb3303b9/grpcio-1.81.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:766bc7c9a9c340342f4c864ccbda8e78111e4751f13b895812b9c148fb79e9d0", size = 6809167, upload-time = "2026-06-11T12:45:32.52Z" }, - { url = "https://files.pythonhosted.org/packages/1c/c7/3030dd940408083bd32cd95d634777a71605ade4887154d93e8a89244946/grpcio-1.81.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b259a04a737cb3496be0901328eb8b7552ed8df4865d8c8f1cf1bffcfc0776a3", size = 7412536, upload-time = "2026-06-11T12:45:35.403Z" }, - { url = "https://files.pythonhosted.org/packages/e0/dd/1172a9e42b168edcafefad6115346ef619a3fc02158bb170e66ced24bcdd/grpcio-1.81.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:85b10a45b8993d195c4f3ff57025b8d1e11834909ee475c403bfa60cb4caefaf", size = 8408276, upload-time = "2026-06-11T12:45:37.78Z" }, - { url = "https://files.pythonhosted.org/packages/25/7a/71437c7f3596e5246155c515852795a85a1a8d228190212432b13b97a95d/grpcio-1.81.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8ea1936c26b99999b27479853039a7f34713f56c49375ad52b38535ec93a796c", size = 7849660, upload-time = "2026-06-11T12:45:40.627Z" }, - { url = "https://files.pythonhosted.org/packages/65/40/7debc0da45d2efebafb82da75644be347497fe4ee250514b8cd3b86ae8bf/grpcio-1.81.1-cp312-cp312-win32.whl", hash = "sha256:a185a04039df6cae8648bc8ab6d6fde7bf94f7188ecf7828e76ac52eef1e41d6", size = 4185819, upload-time = "2026-06-11T12:45:43.027Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b9/8fe3ba5ed462067774ebc1f9c7f26aa7ebcc280ddd476be107153de1339e/grpcio-1.81.1-cp312-cp312-win_amd64.whl", hash = "sha256:3ad74f8bb1a18963914c5452d289422830b39459e8776ebbcd207be1fbfb1d94", size = 4930461, upload-time = "2026-06-11T12:45:45.775Z" }, - { url = "https://files.pythonhosted.org/packages/7a/42/dcc2e4b600538ef18327c0839d56b7d3c3812337c5d710df5877dbb39b1e/grpcio-1.81.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:b10e1ff4756ed27d5a29d7fc79cfce7ef1ff56ad20025b89bac7cf79e09abbbe", size = 6054466, upload-time = "2026-06-11T12:45:48.43Z" }, - { url = "https://files.pythonhosted.org/packages/7b/4a/a36e03210183a8a7d4c80c3936acee679f4bd77d5861f369db47b2cc5f05/grpcio-1.81.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:819edbdcb42ab8598b494bcf0222684bbb7a3c772bd1b1f0be7e029a6063c28e", size = 12048795, upload-time = "2026-06-11T12:45:54.011Z" }, - { url = "https://files.pythonhosted.org/packages/b0/d5/d68e30b29098f63beab6fe501100fe82674ff142b32c672532da86a99b3a/grpcio-1.81.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5bf2dc311127d91230cc79b92188c082634a06cf66c5234db49a43b910183b0", size = 6599094, upload-time = "2026-06-11T12:45:57.799Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b3/e837954d279754f638a11cca5dcf6b24a005efb398984cefaf7735945a54/grpcio-1.81.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e8ca6a1fcdb2943c9cbc1804a1baf3acb6071d72a471591678ded84218006e14", size = 7307182, upload-time = "2026-06-11T12:46:00.568Z" }, - { url = "https://files.pythonhosted.org/packages/0d/1e/b47957057e729adc6cdf519a47f8be2562b7140e280f1418443eb4022192/grpcio-1.81.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e64dd101d380a115cc5a0c7856788adb535f1a4e21fc543775602f8be95180ae", size = 6810962, upload-time = "2026-06-11T12:46:03.312Z" }, - { url = "https://files.pythonhosted.org/packages/40/26/569868e364e05b19ec8f969da53d230bcd89c962cd198f7c29943155c4d3/grpcio-1.81.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:98a07f9bf591e3a8919797bee1c53f026ba4acd587e5a4404c8e57c9ec36b2a5", size = 7415698, upload-time = "2026-06-11T12:46:06.005Z" }, - { url = "https://files.pythonhosted.org/packages/36/0c/5440a0582cb5653fc42a6e262eeb22700943313f8076f9dc927491b20a59/grpcio-1.81.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c261d74b1a945cf895a9d6eccd1685a8e837531beaab782da4d630a8d12deffb", size = 8407779, upload-time = "2026-06-11T12:46:08.84Z" }, - { url = "https://files.pythonhosted.org/packages/ff/aa/66fe9f39871d766987d869a03ee0842a026f499c7b1e62decb9e78a8088e/grpcio-1.81.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58ad1131c300d3c9b933802b3cc4dc69d380822935ba50b28703156ea826fbf7", size = 7844521, upload-time = "2026-06-11T12:46:12.171Z" }, - { url = "https://files.pythonhosted.org/packages/f0/9e/69bb7194861bcd28fb3193261d4f9c3831b4446993f002cf59068943e7ab/grpcio-1.81.1-cp313-cp313-win32.whl", hash = "sha256:78e29211f26da2fdd0e9c6d2b79f489476140cf7029b6a64808ade7ca4156a42", size = 4182786, upload-time = "2026-06-11T12:46:15.192Z" }, - { url = "https://files.pythonhosted.org/packages/0d/20/3da8bb0d637feccdc3e1e419bb511ce93651ce7d54164f95de22cc0b8b34/grpcio-1.81.1-cp313-cp313-win_amd64.whl", hash = "sha256:edb59506291b647a30884b1d51a599d605f40b20af4a7dc3d33786a47a31de60", size = 4928648, upload-time = "2026-06-11T12:46:17.823Z" }, - { url = "https://files.pythonhosted.org/packages/b6/58/19414622b1bf6981bc9c05a365bd548e71876c89000083b3af489251e9c0/grpcio-1.81.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:506f48f2f9c29b143fca3dad7b0d518c188b6c9648c75a2ae6e2d9f2c13a060b", size = 6055336, upload-time = "2026-06-11T12:46:20.557Z" }, - { url = "https://files.pythonhosted.org/packages/32/f1/2ec88adb92b0eba970dd0e0e7dd086341daa3c75eba4f735f9e44bf684b0/grpcio-1.81.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d865db4a6318e1c1bea83292e0ed231090538fc4ca45425b0f0480eb338bbc6e", size = 12056279, upload-time = "2026-06-11T12:46:24.255Z" }, - { url = "https://files.pythonhosted.org/packages/41/36/e8c5f8c6ec71de73733695ebc809e98b178b534ec6d8eaa31a7ebab4ad4c/grpcio-1.81.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2aa72e3ce1770317ef534f63d397b55e130725f5149bd36077c3b539019db27", size = 6608225, upload-time = "2026-06-11T12:46:27.601Z" }, - { url = "https://files.pythonhosted.org/packages/30/22/96fc577a845ab093326d9ab1adb874bd4936c8cf98ac8ed2f3db13a0a2fb/grpcio-1.81.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0490c30c261eded63f3f354979f9dc4502a9fb944cccb60cd9dc85f5a7349854", size = 7306576, upload-time = "2026-06-11T12:46:30.514Z" }, - { url = "https://files.pythonhosted.org/packages/76/7b/61dab5d5969f28d97fb1009cead1df0a5cd987d3315e1b37f18a4449f8bc/grpcio-1.81.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:410482da976329fe5f4067270401b12cf2bd552ff8020f054ecfaddb5475f9d6", size = 6812165, upload-time = "2026-06-11T12:46:33.699Z" }, - { url = "https://files.pythonhosted.org/packages/82/78/6e501929d4f5f96462fd82fd9f0f06e5f9612207582b862868d68757b27d/grpcio-1.81.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3657301562ac3cb8018d30d0d3ebfa39932239f7b5703422057ef14b69949f5", size = 7422962, upload-time = "2026-06-11T12:46:36.511Z" }, - { url = "https://files.pythonhosted.org/packages/2a/7e/f2157589e66daa78ebb3165942d05a08bdea93b9d11c2bc1e172aef89685/grpcio-1.81.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:24c8e57504c8f45b237e40b99262d181071e5099a07053695b75d97bb53053a0", size = 8408176, upload-time = "2026-06-11T12:46:39.803Z" }, - { url = "https://files.pythonhosted.org/packages/da/df/c6717fef716e00d235ffb96123baf6dce76d6004f6233fa767c502861460/grpcio-1.81.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b427c19380991a4eaab2f6144b64b99b412043314c6bf4ab544f97bb31ee4190", size = 7846681, upload-time = "2026-06-11T12:46:43.013Z" }, - { url = "https://files.pythonhosted.org/packages/36/84/3502e9f210a6a5c4438c8aca3f88edd2e04f6a27f3d41b26cf0a0024b096/grpcio-1.81.1-cp314-cp314-win32.whl", hash = "sha256:61233fe8951e5c85dff81c2458b6528624760166946b5b47ea150a589168411f", size = 4264615, upload-time = "2026-06-11T12:46:45.741Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b0/4af731ff7492c68a96e4c71bfd0f4590acde92b31c6fe4894e6465c10ff6/grpcio-1.81.1-cp314-cp314-win_amd64.whl", hash = "sha256:3768a5ff1b2125e6f552e561b6b2dca0e64982d8949689b4df145cf8b98d7821", size = 5070275, upload-time = "2026-06-11T12:46:48.486Z" }, -] - -[[package]] -name = "grpcio-status" -version = "1.81.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "googleapis-common-protos" }, - { name = "grpcio" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/32/26/0aa9168c87882381fd810d140c279a2490ed6aee655f0515d6f56c5ca404/grpcio_status-1.81.1.tar.gz", hash = "sha256:9389a03e746017b10f0630c064289201458f3ce01f5d7ef4b0bebc1ef6cf82ad", size = 13923, upload-time = "2026-06-11T12:58:48.636Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/5e/5abfec5f7e89d3b7993d57cfb025ca5f968a2c18656d7fcda2b6919440b9/grpcio_status-1.81.1-py3-none-any.whl", hash = "sha256:08072fa9995f4a95c647fc6f4f85e2411573d00087bcabdf30f260114338f232", size = 14638, upload-time = "2026-06-11T12:58:31.982Z" }, -] - [[package]] name = "h11" version = "0.16.0" @@ -3614,93 +3298,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" }, ] -[[package]] -name = "opentelemetry-exporter-gcp-logging" -version = "1.12.0a0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-cloud-logging" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-resourcedetector-gcp" }, - { name = "opentelemetry-sdk" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/77/e4/95ecebaa1c5134adaa0d0374028b25e3b3c5c08535d29a66d39d372a3d11/opentelemetry_exporter_gcp_logging-1.12.0a0.tar.gz", hash = "sha256:586529dbbcae5e22b880f7c121fde3f0fe8ae997aba1bad53f13c20eeb27cb3a", size = 22521, upload-time = "2026-04-28T20:59:40.237Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/93/3a0a9a62db0b90029a8160774e791044c0566aa94d5160ce7bbce8abf242/opentelemetry_exporter_gcp_logging-1.12.0a0-py3-none-any.whl", hash = "sha256:2aca9b01b3248c2fa95d38d01aa71aca8e22f640c44dba36ca6b883930762971", size = 14207, upload-time = "2026-04-28T20:59:35.109Z" }, -] - -[[package]] -name = "opentelemetry-exporter-gcp-trace" -version = "1.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-cloud-trace" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-resourcedetector-gcp" }, - { name = "opentelemetry-sdk" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bf/55/32922e72d88421505383dfdba9c1ee6ad67253f94f2358f6e9dbc4ac3749/opentelemetry_exporter_gcp_trace-1.12.0.tar.gz", hash = "sha256:18c6e56fe123eed020d5005fdd819b196d64f651545bce1ca7e2e2cbaf9d343b", size = 18779, upload-time = "2026-04-28T20:59:41.974Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/68/c60e79992918eecb6de167e782c86946fdd5492bb163fe320f1a18959c3d/opentelemetry_exporter_gcp_trace-1.12.0-py3-none-any.whl", hash = "sha256:1538dab654bcb25e757ed34c94f27a2e30d90dc7deb3630f8d46d1111fcb3bad", size = 14013, upload-time = "2026-04-28T20:59:37.518Z" }, -] - -[[package]] -name = "opentelemetry-exporter-otlp-proto-common" -version = "1.42.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-proto" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0e/9c/216acfeaedadf2e1937f4373929b20f73197c5c4a2546d4f584b7fa63813/opentelemetry_exporter_otlp_proto_common-1.42.1.tar.gz", hash = "sha256:04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee", size = 21433, upload-time = "2026-05-21T16:32:55.526Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/43/2375e7612e1121a4518c17603b6e0b03ad94f565aafad53f464dc5be2bf6/opentelemetry_exporter_otlp_proto_common-1.42.1-py3-none-any.whl", hash = "sha256:f48d395ab815b444da118868977e9798ea354c25737d5cf39578ae894011c140", size = 17327, upload-time = "2026-05-21T16:32:33.387Z" }, -] - -[[package]] -name = "opentelemetry-exporter-otlp-proto-http" -version = "1.42.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "googleapis-common-protos" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-common" }, - { name = "opentelemetry-proto" }, - { name = "opentelemetry-sdk" }, - { name = "requests" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/77/32/826bfa1d80ecea24f47808de03cd4a0d13c17ecc07712f45123f0f61e4ac/opentelemetry_exporter_otlp_proto_http-1.42.1.tar.gz", hash = "sha256:bf142a21035d7571ac3a09cb2e5639f49886f243972883cfe777ed3bf02b734d", size = 25406, upload-time = "2026-05-21T16:32:56.807Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/96/82cb223a1502f0787d4bbff12907f5f8d870a50731febcd5818d93ef9555/opentelemetry_exporter_otlp_proto_http-1.42.1-py3-none-any.whl", hash = "sha256:00a16da1b312a1d6c7233d600d557c91df71125af73020f3b9a7765bd699d59d", size = 21793, upload-time = "2026-05-21T16:32:35.277Z" }, -] - -[[package]] -name = "opentelemetry-proto" -version = "1.42.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b4/55/63eac3e1089b768ba014091fdd2ae8a9a440c821ef5e2b786909c94c8836/opentelemetry_proto-1.42.1.tar.gz", hash = "sha256:c6a51e6b4f05ae63565f3a113217f3d2bfaec68f78c02d7a6c85f9010d1cfca6", size = 45839, upload-time = "2026-05-21T16:33:03.937Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/9d/171c02c84a76940b7e601805b3bb536985aded9168fbcc9ba52f0a730fa2/opentelemetry_proto-1.42.1-py3-none-any.whl", hash = "sha256:dedb74cba2886c59c7789b227a7a670613025a07489040050aedff6e5c0fb43c", size = 71782, upload-time = "2026-05-21T16:32:44.867Z" }, -] - -[[package]] -name = "opentelemetry-resourcedetector-gcp" -version = "1.12.0a0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-sdk" }, - { name = "requests" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/21/ae/b62c5e986c9c7f908a15682ea173bcfcdc00403c0c85243ccbd30eca7fc2/opentelemetry_resourcedetector_gcp-1.12.0a0.tar.gz", hash = "sha256:d5e3f78283a272eb92547e00bbeff45b7332a34ae791a70ab4eba81af9bc3baf", size = 18797, upload-time = "2026-04-28T20:59:43.195Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/84/9db2999adbc41505af3e6717e8d958746778cbfc9e07ed9c670bf9d1e6db/opentelemetry_resourcedetector_gcp-1.12.0a0-py3-none-any.whl", hash = "sha256:e803688d14e2969fe816077be81f7b034368314d485863f12ce49daba7c81919", size = 18798, upload-time = "2026-04-28T20:59:39.257Z" }, -] - [[package]] name = "opentelemetry-sdk" version = "1.42.1"