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