• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
117     private const val KEY_FRAME_IDS = "frame_ids"
118     private const val KEY_FEATURES = "features"
119     private const val KEY_FEATURE_NAME = "name"
120     private const val KEY_FEATURE_TYPE = "type"
121     private const val KEY_FEATURE_DATAPOINTS = "data_points"
122 
123     private val unknownType: DataPointType<Any> =
124         DataPointType(
125             "unknown",
<lambda>null126             jsonToValue = { throw UnknownTypeException() },
<lambda>null127             valueToJson = { throw AssertionError() },
128         )
129 }
130 
131 /** Creates a type registry from the types used in the [TimeSeries]. */
<lambda>null132 fun TimeSeries.createTypeRegistry(): Map<String, DataPointType<*>> = buildMap {
133     for (feature in features.values) {
134         for (dataPoint in feature.dataPoints) {
135             if (dataPoint is ValueDataPoint) {
136                 val type = dataPoint.type
137                 val alreadyRegisteredType = put(type.typeName, type)
138                 if (alreadyRegisteredType != null && alreadyRegisteredType != type) {
139                     throw AssertionError(
140                         "Type [${type.typeName}] with multiple different implementations"
141                     )
142                 }
143             }
144         }
145     }
146 }
147 
convertnull148 private fun <I, O> JSONArray.convert(
149     elementAccessor: JSONArray.(index: Int) -> I,
150     convertFn: (I) -> O,
151 ) = buildList {
152     for (i in 0 until length()) {
153         add(convertFn(elementAccessor(i)))
154     }
155 }
156