1 /*
2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package platform.test.motion.golden
18
19 import org.json.JSONArray
20 import org.json.JSONException
21 import org.json.JSONObject
22
23 /**
24 * Utility to (de-)serialize golden [TimeSeries] data in a JSON text format.
25 *
26 * The JSON format is written with human readability in mind.
27 *
28 * Note that this intentionally does not use protocol buffers, since the text format is not
29 * available for the "Protobuf Java Lite Runtime". See http://shortn/_dx5ldOga8s for details.
30 */
31 object JsonGoldenSerializer {
32 /**
33 * Reads a previously JSON serialized [TimeSeries] data.
34 *
35 * Golden data types not included in the `typeRegistry` will produce an [UnknownType].
36 *
37 * @param typeRegistry [DataPointType] implementations used to de-serialize structured JSON
38 * values to golden values. See [TimeSeries.createTypeRegistry] for creating the registry
39 * based on the currently produced timeseries.
40 * @throws JSONException if the JSON data does not match the expected schema.
41 */
fromJsonnull42 fun fromJson(jsonObject: JSONObject, typeRegistry: Map<String, DataPointType<*>>): TimeSeries {
43 val frameIds =
44 jsonObject.getJSONArray(KEY_FRAME_IDS).convert(JSONArray::get, ::frameIdFromJson)
45
46 val features =
47 jsonObject.getJSONArray(KEY_FEATURES).convert(JSONArray::getJSONObject) {
48 featureFromJson(it, typeRegistry)
49 }
50
51 return TimeSeries(frameIds, features)
52 }
53
54 /** Creates a [JSONObject] representing the [golden]. */
toJsonnull55 fun toJson(golden: TimeSeries) =
56 JSONObject().apply {
57 put(
58 KEY_FRAME_IDS,
59 JSONArray().apply { golden.frameIds.map(::frameIdToJson).forEach(this::put) }
60 )
61 put(
62 KEY_FEATURES,
63 JSONArray().apply { golden.features.values.map(::featureToJson).forEach(this::put) }
64 )
65 }
66
frameIdFromJsonnull67 private fun frameIdFromJson(jsonValue: Any): FrameId {
68 return when (jsonValue) {
69 is Number -> TimestampFrameId(jsonValue.toLong())
70 is String -> SupplementalFrameId(jsonValue)
71 else -> throw JSONException("Unknown FrameId type")
72 }
73 }
74
frameIdToJsonnull75 private fun frameIdToJson(frameId: FrameId) =
76 when (frameId) {
77 is TimestampFrameId -> frameId.milliseconds
78 is SupplementalFrameId -> frameId.label
79 }
80
featureFromJsonnull81 private fun featureFromJson(
82 jsonObject: JSONObject,
83 typeRegistry: Map<String, DataPointType<*>>
84 ): Feature<*> {
85 val name = jsonObject.getString(KEY_FEATURE_NAME)
86 val type = typeRegistry[jsonObject.optString(KEY_FEATURE_TYPE)] ?: unknownType
87
88 val dataPoints =
89 jsonObject.getJSONArray(KEY_FEATURE_DATAPOINTS).convert(JSONArray::get, type::fromJson)
90 return Feature(name, dataPoints)
91 }
92
featureToJsonnull93 private fun featureToJson(feature: Feature<*>) =
94 JSONObject().apply {
95 put(KEY_FEATURE_NAME, feature.name)
96
97 val dataPointTypes =
98 feature.dataPoints
99 .filterIsInstance<ValueDataPoint<Any>>()
100 .map { it.type.typeName }
101 .toSet()
102 if (dataPointTypes.size == 1) {
103 put(KEY_FEATURE_TYPE, dataPointTypes.single())
104 } else if (dataPointTypes.size > 1) {
105 throw JSONException(
106 "Feature [${feature.name}] contains more than one data point type: " +
107 "[${dataPointTypes.joinToString()}]"
108 )
109 }
110
111 put(
112 KEY_FEATURE_DATAPOINTS,
113 JSONArray().apply { feature.dataPoints.map { it.asJson() }.forEach(this::put) }
114 )
115 }
116 private const val KEY_FRAME_IDS = "frame_ids"
117 private const val KEY_FEATURES = "features"
118 private const val KEY_FEATURE_NAME = "name"
119 private const val KEY_FEATURE_TYPE = "type"
120 private const val KEY_FEATURE_DATAPOINTS = "data_points"
121
122 private val unknownType: DataPointType<Any> =
123 DataPointType(
124 "unknown",
<lambda>null125 jsonToValue = { throw UnknownTypeException() },
<lambda>null126 valueToJson = { throw AssertionError() }
127 )
128 }
129
130 /** Creates a type registry from the types used in the [TimeSeries]. */
<lambda>null131 fun TimeSeries.createTypeRegistry(): Map<String, DataPointType<*>> = buildMap {
132 for (feature in features.values) {
133 for (dataPoint in feature.dataPoints) {
134 if (dataPoint is ValueDataPoint) {
135 val type = dataPoint.type
136 val alreadyRegisteredType = put(type.typeName, type)
137 if (alreadyRegisteredType != null && alreadyRegisteredType != type) {
138 throw AssertionError(
139 "Type [${type.typeName}] with multiple different implementations"
140 )
141 }
142 }
143 }
144 }
145 }
146
convertnull147 private fun <I, O> JSONArray.convert(
148 elementAccessor: JSONArray.(index: Int) -> I,
149 convertFn: (I) -> O
150 ) = buildList {
151 for (i in 0 until length()) {
152 add(convertFn(elementAccessor(i)))
153 }
154 }
155