diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c639763b0..608132bd5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ [Full changelog](https://github.com/mozilla/glean/compare/v66.0.0...main) +* Swift + * Rely on `Codable` again for serialization of object metrics ([#3300](https://github.com/mozilla/glean/pull/3300)) + # v66.0.0 (2025-10-20) [Full changelog](https://github.com/mozilla/glean/compare/v65.2.2...v66.0.0) diff --git a/Cargo.lock b/Cargo.lock index fb08c4eee7..eaf292db97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -378,7 +378,7 @@ dependencies = [ [[package]] name = "glean-build" -version = "18.0.2" +version = "18.0.6" dependencies = [ "tempfile", "xshell-venv", diff --git a/glean-core/Cargo.toml b/glean-core/Cargo.toml index d1c6409b3b..ddf0bd9fd2 100644 --- a/glean-core/Cargo.toml +++ b/glean-core/Cargo.toml @@ -21,7 +21,7 @@ include = [ rust-version = "1.82" [package.metadata.glean] -glean-parser = "18.0.2" +glean-parser = "18.0.6" [badges] circle-ci = { repository = "mozilla/glean", branch = "main" } diff --git a/glean-core/build/Cargo.toml b/glean-core/build/Cargo.toml index baf4dc27f9..03cbd66608 100644 --- a/glean-core/build/Cargo.toml +++ b/glean-core/build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "glean-build" -version = "18.0.2" +version = "18.0.6" edition = "2021" description = "Glean SDK Rust build helper" repository = "https://github.com/mozilla/glean" diff --git a/glean-core/build/src/lib.rs b/glean-core/build/src/lib.rs index 435adc8239..4be407e7a6 100644 --- a/glean-core/build/src/lib.rs +++ b/glean-core/build/src/lib.rs @@ -39,7 +39,7 @@ use std::{env, path::PathBuf}; use xshell_venv::{Result, Shell, VirtualEnv}; -const GLEAN_PARSER_VERSION: &str = "18.0.2"; +const GLEAN_PARSER_VERSION: &str = "18.0.6"; /// A Glean Rust bindings generator. pub struct Builder { diff --git a/glean-core/ios/Glean/Metrics/ObjectMetric.swift b/glean-core/ios/Glean/Metrics/ObjectMetric.swift index 4cdb44c613..bd6cdf8c88 100644 --- a/glean-core/ios/Glean/Metrics/ObjectMetric.swift +++ b/glean-core/ios/Glean/Metrics/ObjectMetric.swift @@ -5,27 +5,11 @@ /// An object that can be serialized into JSON. /// /// Objects are defined by their structure in the metrics definition. -public protocol ObjectSerialize: Decodable { +public protocol ObjectSerialize: Codable { func intoSerializedObject() -> String } -extension Array: ObjectSerialize where Element: ObjectSerialize { - public func intoSerializedObject() -> String { - var json = "[" - var first = true - for elem in self { - if !first { - json.append(",") - } - first = false - json.append(elem.intoSerializedObject()) - } - json.append("]") - return json - } -} - -extension String: ObjectSerialize { +extension Array: ObjectSerialize where Element: Codable { public func intoSerializedObject() -> String { let jsonEncoder = JSONEncoder() let jsonData = try! jsonEncoder.encode(self) @@ -34,18 +18,6 @@ extension String: ObjectSerialize { } } -extension Bool: ObjectSerialize { - public func intoSerializedObject() -> String { - return self ? "true" : "false" - } -} - -extension Int64: ObjectSerialize { - public func intoSerializedObject() -> String { - return String(self) - } -} - /// This implements the developer facing API for the object metric type. /// /// Instances of this class type are automatically generated by the parsers at built time, diff --git a/glean-core/ios/GleanTests/Metrics/ObjectMetricTests.swift b/glean-core/ios/GleanTests/Metrics/ObjectMetricTests.swift index 5eeac99ed9..b093d24e72 100644 --- a/glean-core/ios/GleanTests/Metrics/ObjectMetricTests.swift +++ b/glean-core/ios/GleanTests/Metrics/ObjectMetricTests.swift @@ -5,30 +5,82 @@ @testable import Glean import XCTest -struct BalloonsObjectItem: Decodable, Equatable, ObjectSerialize { +struct BalloonsObjectItem: Codable, Equatable { var colour: String? var diameter: Int64? + var anotherValue: Bool? - func intoSerializedObject() -> String { - var data: [String] = [] - if let val = self.colour { - var elem = "\"colour\":" - elem.append(val.intoSerializedObject()) - data.append(elem) + enum CodingKeys: String, CodingKey { + case colour = "colour" + case diameter = "diameter" + case anotherValue = "another_value" + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + if let colour = self.colour { + try container.encode(colour, forKey: .colour) } - if let val = self.diameter { - var elem = "\"diameter\":" - elem.append(val.intoSerializedObject()) - data.append(elem) + if let diameter = self.diameter { + try container.encode(diameter, forKey: .diameter) + } + if let anotherValue = self.anotherValue { + try container.encode(anotherValue, forKey: .anotherValue) } - let obj = data.joined(separator: ",") - let json = "{" + obj + "}" - return json } } - typealias BalloonsObject = [BalloonsObjectItem] +// generated from +// +// ``` +// structure: +// type: object +// properties: +// key1: +// type: string +// another_value: +// type: number +// sub_array: +// type: array +// items: +// type: number +// ``` +struct ToplevelObjectObject: Codable, Equatable, ObjectSerialize { + var key1: String? + var anotherValue: Int64? + var subArray: ToplevelObjectObjectSubArray = [] + + enum CodingKeys: String, CodingKey { + case key1 = "key1" + case anotherValue = "another_value" + case subArray = "sub_array" + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + if let key1 = self.key1 { + try container.encode(key1, forKey: .key1) + } + if let anotherValue = self.anotherValue { + try container.encode(anotherValue, forKey: .anotherValue) + } + if subArray.count > 0 { + let subArray = self.subArray + try container.encode(subArray, forKey: .subArray) + } + } + + func intoSerializedObject() -> String { + let jsonEncoder = JSONEncoder() + let jsonData = try! jsonEncoder.encode(self) + let json = String(data: jsonData, encoding: String.Encoding.utf8)! + return json + } +} +typealias ToplevelObjectObjectSubArray = [ToplevelObjectObjectSubArrayItem] +typealias ToplevelObjectObjectSubArrayItem = Int64 + class ObjectMetricTypeTests: XCTestCase { override func setUp() { resetGleanDiscardingInitialPings(testCase: self, tag: "ObjectMetricTypeTests") @@ -132,4 +184,54 @@ class ObjectMetricTypeTests: XCTestCase { XCTAssertEqual(2, snapshot.count) XCTAssertEqual(expected, snapshot) } + + func testObjectDecodesFromSnakeCase() { + let metric = ObjectMetricType(CommonMetricData( + category: "test", + name: "balloon", + sendInPings: ["store1"], + lifetime: .ping, + disabled: false + )) + + XCTAssertNil(metric.testGetValue()) + + var balloons: BalloonsObject = [] + balloons.append(BalloonsObjectItem(colour: "red", diameter: 5, anotherValue: true)) + balloons.append(BalloonsObjectItem(colour: "green", anotherValue: false)) + metric.set(balloons) + + let snapshot = metric.testGetValue()! + XCTAssertNotNil(snapshot) + XCTAssertEqual(2, snapshot.count) + + XCTAssertEqual(snapshot[0].colour, "red") + XCTAssertEqual(snapshot[0].diameter, 5) + XCTAssertEqual(snapshot[0].anotherValue, true) + XCTAssertEqual(snapshot[1].colour, "green") + XCTAssertNil(snapshot[1].diameter) + XCTAssertEqual(snapshot[1].anotherValue, false) + } + + func testObjectWithStructureOnToplevel() { + let metric = ObjectMetricType(CommonMetricData( + category: "test", + name: "toplevel_object", + sendInPings: ["store1"], + lifetime: .ping, + disabled: false + )) + + XCTAssertNil(metric.testGetValue()) + + let obj = ToplevelObjectObject(key1: "test", anotherValue: 3, subArray: [1, 2, 3]) + metric.set(obj) + + let snapshot = metric.testGetValue()! + XCTAssertNotNil(snapshot) + + XCTAssertEqual("test", snapshot.key1) + XCTAssertEqual(3, snapshot.anotherValue) + XCTAssertEqual([1, 2, 3], snapshot.subArray) + } } diff --git a/glean-core/python/glean/__init__.py b/glean-core/python/glean/__init__.py index d39977b47c..d7cb7566b8 100644 --- a/glean-core/python/glean/__init__.py +++ b/glean-core/python/glean/__init__.py @@ -30,7 +30,7 @@ __email__ = "glean-team@mozilla.com" -GLEAN_PARSER_VERSION = "18.0.2" +GLEAN_PARSER_VERSION = "18.0.6" parser_version = VersionInfo.parse(GLEAN_PARSER_VERSION) parser_version_next_major = parser_version.bump_major() diff --git a/samples/ios/app/glean-sample-app/AppDelegate.swift b/samples/ios/app/glean-sample-app/AppDelegate.swift index f3501abba2..dea44e8861 100644 --- a/samples/ios/app/glean-sample-app/AppDelegate.swift +++ b/samples/ios/app/glean-sample-app/AppDelegate.swift @@ -42,6 +42,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { glean.initialize(uploadEnabled: true, buildInfo: GleanMetrics.GleanBuild.info) } + glean.setDebugViewTag("jer-ios") + Test.timespan.start() // Set a sample value for a metric. diff --git a/samples/ios/app/glean-sample-app/ViewController.swift b/samples/ios/app/glean-sample-app/ViewController.swift index a30ce7af1a..a6c423a9bd 100644 --- a/samples/ios/app/glean-sample-app/ViewController.swift +++ b/samples/ios/app/glean-sample-app/ViewController.swift @@ -77,6 +77,9 @@ class ViewController: UIViewController { ch.append(f) Party.chooser.set(ch) + let tlObj = Party.ToplevelObjectObject(key1: "test", anotherValue: 3, subArray: [1, 2, 3]) + Party.toplevelObject.set(tlObj) + // This is referencing the event ping named 'click' from the metrics.yaml file. In // order to illustrate adding extra information to the event, it is also adding to the // 'extras' field a dictionary of values. Note that the dictionary keys must be diff --git a/samples/ios/app/glean-sample-appUITests/ViewControllerTest.swift b/samples/ios/app/glean-sample-appUITests/ViewControllerTest.swift index 4504a096a5..333d71a8ea 100644 --- a/samples/ios/app/glean-sample-appUITests/ViewControllerTest.swift +++ b/samples/ios/app/glean-sample-appUITests/ViewControllerTest.swift @@ -57,6 +57,14 @@ class ViewControllerTest: XCTestCase { let chooser = objects["party.chooser"] as! [[String: AnyHashable]] XCTAssertEqual(expectedChooser, chooser) + + let obj = objects["party.toplevel_object"] as! [String: AnyHashable] + let expectedObj: [String: AnyHashable] = [ + "key1": "test", + "another_value": 3, + "sub_array": [1, 2, 3] + ] + XCTAssertEqual(expectedObj, obj) } func testViewControllerInteraction() { diff --git a/samples/ios/app/metrics.yaml b/samples/ios/app/metrics.yaml index 82899d3d6f..405689252c 100644 --- a/samples/ios/app/metrics.yaml +++ b/samples/ios/app/metrics.yaml @@ -241,3 +241,27 @@ party: - type: string - type: number - type: boolean + + toplevel_object: + type: object + description: Top-level object + bugs: + - https://bugzilla.mozilla.org/11137353 + data_reviews: + - http://example.com/reviews + notification_emails: + - CHANGE-ME@example.com + expires: never + send_in_pings: + - sample + structure: + type: object + properties: + key1: + type: string + another_value: + type: number + sub_array: + type: array + items: + type: number