diff --git a/.gitignore b/.gitignore index fe25a78..a5b8309 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,6 @@ bin/ ### Mac OS ### .DS_Store timeline/.idea/copilot/ + +### Project Specific Files ### +**/resources/*.schema.json diff --git a/parser/src/main/kotlin/model/Records.kt b/parser/src/main/kotlin/model/Records.kt index ca429d5..26f6727 100644 --- a/parser/src/main/kotlin/model/Records.kt +++ b/parser/src/main/kotlin/model/Records.kt @@ -162,7 +162,7 @@ data class Activity( /** * Type of activity detected. */ - val type: String?, + val type: ActivityType = ActivityType.UNKNOWN_ACTIVITY_TYPE, /** * Value from 0 to 100 indicating the likelihood that the user is performing this activity. * The larger the value, the more consistent the data used to perform the classification is with the detected activity. @@ -170,3 +170,51 @@ data class Activity( */ val confidence: Int?, ) + +/** + * Enum class for different types of activities detected by the device, used in [Activity.type]. + */ +enum class ActivityType(val title: String, val extraColor: String) { + BOATING("Boating", "#01579b"), + CATCHING_POKEMON("Catching Pokémon", "#db4437"), + CYCLING("Cycling", "#4db6ac"), + FLYING("Flying", "#3f51b5"), + HIKING("Hiking", "#c2185b"), + HORSEBACK_RIDING("Horseback riding", "#4db6ac"), + IN_BUS("On a bus", "#01579b"), + IN_CABLECAR("In a cable car", "#01579b"), + IN_FERRY("On a ferry", "#01579b"), + IN_FUNICULAR("On a funicular", "#01579b"), + IN_GONDOLA_LIFT("In a gondola lift", "#01579b"), + IN_PASSENGER_VEHICLE("Driving", "#01579b"), + IN_SUBWAY("On the subway", "#01579b"), + IN_TAXI("In a taxi", "#01579b"), + IN_TRAIN("On a train", "#01579b"), + IN_TRAM("On a tram", "#01579b"), + IN_VEHICLE("In a vehicle", "#03a9f4"), + IN_WHEELCHAIR("By wheelchair", "#03a9f4"), + KAYAKING("Kayaking", "#4db6ac"), + KITESURFING("Kitesurfing", "#4db6ac"), + MOTORCYCLING("Motorcycling", "#01579b"), + ON_FOOT("On foot", "#c2185b"), + ON_BICYCLE("On a bicycle", "#4db6ac"), + PARAGLIDING("Paragliding", "#4db6ac"), + ROWING("Rowing", "#c2185b"), + RUNNING("Running", "#c2185b"), + SAILING("Sailing", "#4db6ac"), + SKATEBOARDING("Skateboarding", "#4db6ac"), + SKATING("Skating", "#4db6ac"), + SKIING("Skiing", "#4db6ac"), + SLEDDING("Sledding", "#4db6ac"), + SNOWBOARDING("Snowboarding", "#4db6ac"), + SNOWMOBILE("Snowmobiling", "#01579b"), + SNOWSHOEING("Snowshoeing", "#c2185b"), + STILL("Still", "#01579b"), + SURFING("Surfing", "#4db6ac"), + SWIMMING("Swimming", "#c2185b"), + TILTING("Tilting", "#01579b"), + UNKNOWN("Unknown", "#03a9f4"), + UNKNOWN_ACTIVITY_TYPE("Unknown Activity", "#03a9f4"), + WALKING("Walking", "#03a9f4"), + WALKING_NORDIC("Nordic walking", "#c2185b"), +} diff --git a/parser/src/main/kotlin/model/Semantic.kt b/parser/src/main/kotlin/model/Semantic.kt index 0e8cb33..22f9119 100644 --- a/parser/src/main/kotlin/model/Semantic.kt +++ b/parser/src/main/kotlin/model/Semantic.kt @@ -2,7 +2,13 @@ package dev.hossain.timeline.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import java.time.ZonedDateTime +/** + * A Semantic Location History JSON file in a Google Takeout Location History extraction. + * Contains Semantic Location History information from the user's account. + * Typically, this will be a file containing data for a single month, with a name such as `2021_JANUARY.json`. + */ @JsonClass(generateAdapter = true) data class SemanticTimeline( @Json(name = "timelineObjects") @@ -80,7 +86,7 @@ data class ActivitySegment( * Example: "CONFIRMED" */ @Json(name = "editConfirmationStatus") - val editConfirmationStatus: String?, + val editConfirmationStatus: EditConfirmationStatus?, /** * Edit-Action Metadata for this activity segment * Example: EditActionMetadata(lastEditedTimestamp = "2022-03-06T14:13:11.092Z") @@ -95,23 +101,160 @@ data class ActivitySegment( val lastEditedTimestamp: String?, ) +/** + * Represents the edit confirmation status. + */ +enum class EditConfirmationStatus { + /** + * Indicates the user has not manually edited the entry. + */ + NOT_CONFIRMED, + + /** + * Indicates the user has manually edited the entry. + */ + CONFIRMED, +} + +/** + * Represents a location with various properties. + * + * Example: + * ```json + * { + * "latitudeE7": 414036299, + * "longitudeE7": 21743558, + * "placeId": "ChIJk_s92NyipBIRUMnDG8Kq2Js", + * "address": "C/ de Mallorca, 401\n08013 Barcelona\nEspanya", + * "name": "La Sagrada Familia", + * "semanticType": "TYPE_SEARCHED_ADDRESS", + * "sourceInfo": { + * "deviceTag": 1234567890 + * }, + * "locationConfidence": 87.07311, + * "calibratedProbability": 76.20023 + * } + * ``` + */ @JsonClass(generateAdapter = true) data class Location( - /** Latitude coordinate of the location */ - @Json(name = "latitudeE7") - val latitudeE7: Int, - /** Longitude coordinate of the location */ - @Json(name = "longitudeE7") - val longitudeE7: Int, - /** Google Maps Place ID of the location */ - @Json(name = "placeId") - val placeId: String?, - /** Address of the location */ - @Json(name = "address") - val address: String?, - /** Name of the location */ - @Json(name = "name") - val name: String?, + /** + * Latitude coordinate of the location. Degrees multiplied by 10^7 and rounded to the nearest integer. + * Example: 414216106 + */ + @Json(name = "latitudeE7") val latitudeE7: Int, + /** + * Longitude coordinate of the location. Degrees multiplied by 10^7 and rounded to the nearest integer. + * Example: 21684775 + */ + @Json(name = "longitudeE7") val longitudeE7: Int, + /** + * Google Maps Place ID of the location. + * Example: "ChIJk_s92NyipBIRUMnDG8Kq2Js" + */ + @Json(name = "placeId") val placeId: String?, + /** + * Address of the location. + * Example: "C/ de Mallorca, 401\n08013 Barcelona\nEspanya" + */ + @Json(name = "address") val address: String?, + /** + * Name of the location. + * Example: "La Sagrada Familia" + */ + @Json(name = "name") val name: String?, + /** + * Place type based on semantic information specific to the user. + * Example: "TYPE_HOME" + */ + @Json(name = "semanticType") val semanticType: SemanticType?, + /** + * Approximate accuracy radius of the location measurement, in meters. A lower value means better precision. + * Example: 19 + */ + @Json(name = "accuracyMetres") val accuracyMetres: Int?, + /** + * Confidence in the location. + * Example: 100.0 + */ + @Json(name = "locationConfidence") val locationConfidence: Double?, + /** + * Whether this is the current location. + * Example: true + */ + @Json(name = "isCurrentLocation") val isCurrentLocation: Boolean?, + /** + * Calibrated probability of the location. + * Example: 100.0 + */ + @Json(name = "calibratedProbability") val calibratedProbability: Double?, + /** + * Information on the source that provided the location. + * Example: + * { + * "deviceTag": 1234567890 + * } + */ + @Json(name = "sourceInfo") val sourceInfo: DeviceSourceInfo?, +) + +/** + * Place type based on semantic information specific to the user. + * + * Example: + * ```json + * { + * "semanticType": "TYPE_HOME" + * } + * ``` + */ +enum class SemanticType(val title: String, val description: String, val extraColor: String) { + @Json(name = "TYPE_HOME") + TYPE_HOME( + "Type Home", + "The place has been designated as 'Home' by the user.", + "#03a9f4", + ), + + @Json(name = "TYPE_WORK") + TYPE_WORK( + "Type Work", + "The place has been designated as 'Work' by the user.", + "#03a9f4", + ), + + @Json(name = "TYPE_SEARCHED_ADDRESS") + TYPE_SEARCHED_ADDRESS( + "Type Searched Address", + "The user has searched for this place in the past.", + "#03a9f4", + ), + + @Json(name = "TYPE_ALIASED_LOCATION") + TYPE_ALIASED_LOCATION( + "Type Aliased Location", + "The place has been given a private label by the user.", + "#03a9f4", + ), +} + +/** + * Represents source information of a location. + * + * Example: + * ```json + * { + * "deviceTag": 1234567890 + * } + * ``` + */ +@JsonClass(generateAdapter = true) +data class DeviceSourceInfo( + /** + * Integer identifier associated with the device that obtained the location. + * Example: 1234567890 + */ + @Json(name = "deviceTag") val deviceTag: Int, ) @JsonClass(generateAdapter = true) @@ -230,24 +373,28 @@ data class PlaceVisit( val checkin: Checkin?, ) +/** + * Represents a duration of time defined by a start timestamp and an end timestamp. + * Example: + * ```json + * { + * "startTimestamp": "2022-02-02T10:41:08.315Z", + * "endTimestamp": "2022-02-02T10:45:09.962Z" + * } + * ``` + */ @JsonClass(generateAdapter = true) data class Duration( - /** Start timestamp of the duration */ - @Json(name = "startTimestamp") - val startTimestamp: String, - /** End timestamp of the duration */ - @Json(name = "endTimestamp") - val endTimestamp: String, -) - -@JsonClass(generateAdapter = true) -data class ActivityDuplicate( - /** Type of activity */ - @Json(name = "activityType") - val activityType: String, - /** Probability that the activity type is correct */ - @Json(name = "probability") - val probability: Double, + /** + * Start timestamp of the duration. + * Example: "2022-02-02T10:41:08.315Z" + */ + @Json(name = "startTimestamp") val startTimestamp: ZonedDateTime, + /** + * End timestamp of the duration. + * Example: "2022-02-02T10:45:09.962Z" + */ + @Json(name = "endTimestamp") val endTimestamp: ZonedDateTime, ) @JsonClass(generateAdapter = true) @@ -264,14 +411,17 @@ data class SimplifiedRawPath( val points: List, ) +/** + * A path taken in a public transit system, such as a bus or a metro. + * Note that it does not describe an entire transit line, but only a specific journey a user does in a transit line. + */ @JsonClass(generateAdapter = true) data class TransitPath( /** * List of locations of the transit stops used. - * Example: [TransitStop(latitudeE7 = 414083140, longitudeE7 = 21704000, placeId = "ChIJWey1zMWipBIRiNQSzpI4EDQ", address = "08025 Barcelona\nEspaña", name = "Sant Antoni Maria Claret-Lepant")] */ @Json(name = "transitStops") - val transitStops: List, + val transitStops: List, /** * Name of the transit line. * Example: "H8" @@ -285,14 +435,13 @@ data class TransitPath( @Json(name = "hexRgbColor") val hexRgbColor: String, /** - * Google Maps Place ID of the transit line. + * Google Maps [Place ID](https://developers.google.com/maps/documentation/places/web-service/place-id) of the transit line. * Example: "ChIJQVEUoLuipBIRJO37wI4yyBs" */ @Json(name = "linePlaceId") val linePlaceId: String, /** * Time information (departure and arrival times, both real and scheduled) for each transit stop used. - * Example: [StopTimeInfo(scheduledDepartureTimestamp = "2022-03-03T12:42:00Z", realtimeDepartureTimestamp = "2022-03-03T12:43:37Z")] */ @Json(name = "stopTimesInfo") val stopTimesInfo: List, @@ -303,7 +452,7 @@ data class TransitPath( @Json(name = "source") val source: String, /** - * Confidence of the transit path. + * Confidence level of the transit path data. Ranges from 0 to 1. * Example: 0.9155850640140931 */ @Json(name = "confidence") @@ -332,24 +481,133 @@ data class StopTimeInfo( val realtimeDepartureTimestamp: String, ) +/** + * Represents a parking event. + * Example: + * ```json + * { + * "location": { + * "latitudeE7": 412518975, + * "longitudeE7": 21683133, + * "accuracyMetres": 19 + * }, + * "method": "EXITING_VEHICLE_SIGNAL", + * "locationSource": "FROM_RAW_LOCATION", + * "timestamp": "2022-02-27T14:47:16.731Z" + * } + * ``` + */ @JsonClass(generateAdapter = true) data class ParkingEvent( - /** The location of the parking event */ + /** + * Location of the parking event. + */ @Json(name = "location") val location: Location, - /** The duration of the parking event */ - @Json(name = "duration") - val duration: Duration?, + /** + * Method of parking event detection. + * Example: "EXITING_VEHICLE_SIGNAL" + */ + @Json(name = "method") + val method: Method, + /** + * Source of the location data. + * Example: "FROM_RAW_LOCATION" + */ + @Json(name = "locationSource") + val locationSource: LocationSource, + /** + * Timestamp of the parking event. + * Example: "2022-02-27T14:47:16.731Z" + */ + @Json(name = "timestamp") + val timestamp: ZonedDateTime, ) +enum class Method { + @Json(name = "PERSONAL_VEHICLE_CONFIDENCE") + PERSONAL_VEHICLE_CONFIDENCE, + + @Json(name = "END_OF_ACTIVITY_SEGMENT") + END_OF_ACTIVITY_SEGMENT, + + @Json(name = "EXITING_VEHICLE_SIGNAL") + EXITING_VEHICLE_SIGNAL, + + @Json(name = "UNDEFINED") + UNDEFINED, +} + +enum class LocationSource { + @Json(name = "FROM_RAW_LOCATION") + FROM_RAW_LOCATION, + + @Json(name = "NOT_FROM_RAW_LOCATION") + NOT_FROM_RAW_LOCATION, + + @Json(name = "UNKNOWN") + UNKNOWN, +} + +/** + * Represents the edit action metadata. + */ @JsonClass(generateAdapter = true) data class EditActionMetadata( - /** The action of the edit */ - @Json(name = "action") - val action: String, - /** The timestamp of the edit action */ - @Json(name = "actionTimestamp") - val actionTimestamp: String, + /** + * Represents the activity segment. + */ + @Json(name = "activitySegment") + val activitySegment: ActivitySegment, + /** + * Represents the place visit segment. + */ + @Json(name = "placeVisitSegment") + val placeVisitSegment: PlaceVisitSegment, + /** + * Represents the edit history. + */ + @Json(name = "editHistory") + val editHistory: EditHistory, + /** + * Represents the original candidates. + */ + @Json(name = "originalCandidates") + val originalCandidates: OriginalCandidates, +) + +@JsonClass(generateAdapter = true) +data class PlaceVisitSegment( + @Json(name = "location") + val location: Location, +) + +@JsonClass(generateAdapter = true) +data class EditHistory( + @Json(name = "editEvent") + val editEvent: List, +) + +@JsonClass(generateAdapter = true) +data class EditEvent( + @Json(name = "editOperation") + val editOperation: List, + @Json(name = "uiConfiguration") + val uiConfiguration: UIConfiguration, +) + +@JsonClass(generateAdapter = true) +data class UIConfiguration( + @Json(name = "uiActivitySegmentConfiguration") + val uiActivitySegmentConfiguration: String, + @Json(name = "uiPlaceVisitConfiguration") + val uiPlaceVisitConfiguration: String, +) + +@JsonClass(generateAdapter = true) +data class OriginalCandidates( + @Json(name = "placeVisitSegment") + val placeVisitSegment: PlaceVisitSegment, ) @JsonClass(generateAdapter = true) @@ -362,51 +620,31 @@ data class Checkin( val placeId: String, ) +/** + * Represents a point with latitude, longitude, accuracy in meters, and timestamp. + */ @JsonClass(generateAdapter = true) -data class TransitStop( - /** - * Latitude coordinate of the transit stop. Degrees multiplied by 10^7 and rounded to the nearest integer, in the range -900000000 to +900000000 (divide value by 10^7 for the usual range -90° to +90°). - * Example: 414083140 - */ - @Json(name = "latitudeE7") - val latitudeE7: Int, +data class Point( /** - * Longitude coordinate of the transit stop. Degrees multiplied by 10^7 and rounded to the nearest integer, in the range -1800000000 to +1800000000 (divide value by 10^7 for the usual range -180° to +180°). - * Example: 21704000 + * Latitude coordinate of the point. Degrees multiplied by 10^7 and rounded to the nearest integer, in the range -900000000 to +900000000 (divide value by 10^7 for the usual range -90° to +90°). Example: 414216106 */ - @Json(name = "longitudeE7") - val longitudeE7: Int, + @Json(name = "latE7") + val latE7: Int, /** - * Google Maps Place ID of the transit stop. - * Example: "ChIJWey1zMWipBIRiNQSzpI4EDQ" + * Longitude coordinate of the point. Degrees multiplied by 10^7 and rounded to the nearest integer, in the range -1800000000 to +1800000000 (divide value by 10^7 for the usual range -180° to +180°). Example: 21684775 */ - @Json(name = "placeId") - val placeId: String, + @Json(name = "lngE7") + val lngE7: Int, /** - * Address of the transit stop. - * Example: "08025 Barcelona\nEspaña" + * Approximate accuracy radius of the location measurement, in meters. A lower value means better precision. Example: 10 */ - @Json(name = "address") - val address: String, + @Json(name = "accuracyMeters") + val accuracyMeters: Int, /** - * Name of the transit stop. - * Example: "Sant Antoni Maria Claret-Lepant" + * Timestamp of the point. Example: "2022-03-03T08:27:48Z" */ - @Json(name = "name") - val name: String, -) - -@JsonClass(generateAdapter = true) -data class Point( - /** The latitude of the point */ - @Json(name = "latE7") - val latE7: Int, - /** The longitude of the point */ - @Json(name = "lngE7") - val lngE7: Int, - /** The timestamp of the point */ @Json(name = "timestamp") - val timestamp: String?, + val timestamp: ZonedDateTime, ) @JsonClass(generateAdapter = true) @@ -465,17 +703,18 @@ data class RoadSegment( val placeId: String, ) +/** + * Represents a waypoint with latitude and longitude. + */ @JsonClass(generateAdapter = true) data class Waypoint( /** - * Latitude coordinate of the waypoint. Degrees multiplied by 10^7 and rounded to the nearest integer, in the range -900000000 to +900000000 (divide value by 10^7 for the usual range -90° to +90°). - * Example: 414216106 + * Latitude coordinate of the waypoint. Degrees multiplied by 10^7 and rounded to the nearest integer, in the range -900000000 to +900000000 (divide value by 10^7 for the usual range -90° to +90°). Example: 414216106 */ @Json(name = "latE7") val latE7: Int, /** - * Longitude coordinate of the waypoint. Degrees multiplied by 10^7 and rounded to the nearest integer, in the range -1800000000 to +1800000000 (divide value by 10^7 for the usual range -180° to +180°). - * Example: 21684775 + * Longitude coordinate of the waypoint. Degrees multiplied by 10^7 and rounded to the nearest integer, in the range -1800000000 to +1800000000 (divide value by 10^7 for the usual range -180° to +180°). Example: 21684775 */ @Json(name = "lngE7") val lngE7: Int, diff --git a/parser/src/main/kotlin/moshi/ZonedDateTimeAdapter.kt b/parser/src/main/kotlin/moshi/ZonedDateTimeAdapter.kt new file mode 100644 index 0000000..e7afbb7 --- /dev/null +++ b/parser/src/main/kotlin/moshi/ZonedDateTimeAdapter.kt @@ -0,0 +1,19 @@ +import com.squareup.moshi.FromJson +import com.squareup.moshi.ToJson +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +/** + * Moshi adapter for [ZonedDateTime]. + */ +class ZonedDateTimeAdapter { + @ToJson + fun toJson(zonedDateTime: ZonedDateTime): String { + return DateTimeFormatter.ISO_ZONED_DATE_TIME.format(zonedDateTime) + } + + @FromJson + fun fromJson(zonedDateTimeString: String): ZonedDateTime { + return ZonedDateTime.parse(zonedDateTimeString, DateTimeFormatter.ISO_ZONED_DATE_TIME) + } +} diff --git a/parser/src/test/kotlin/dev/hossain/timeline/model/RecordsTest.kt b/parser/src/test/kotlin/dev/hossain/timeline/model/RecordsTest.kt index 0054c1a..8e0643c 100644 --- a/parser/src/test/kotlin/dev/hossain/timeline/model/RecordsTest.kt +++ b/parser/src/test/kotlin/dev/hossain/timeline/model/RecordsTest.kt @@ -1,5 +1,6 @@ package dev.hossain.timeline.model +import ZonedDateTimeAdapter import com.google.common.truth.Truth.assertThat import com.squareup.moshi.Moshi import kotlin.test.Test @@ -8,7 +9,10 @@ import kotlin.test.Test * Test cases for [Settings]. */ class RecordsTest { - private val moshi: Moshi = Moshi.Builder().build() + private val moshi: Moshi = + Moshi.Builder() + .add(ZonedDateTimeAdapter()) + .build() @Test fun `given records json should parse all records`() { diff --git a/parser/src/test/kotlin/dev/hossain/timeline/model/RecordsTestCopilot.kt b/parser/src/test/kotlin/dev/hossain/timeline/model/RecordsTestCopilot.kt index c756d51..ecddc19 100644 --- a/parser/src/test/kotlin/dev/hossain/timeline/model/RecordsTestCopilot.kt +++ b/parser/src/test/kotlin/dev/hossain/timeline/model/RecordsTestCopilot.kt @@ -100,17 +100,17 @@ class RecordsTestCopilot { fun `Activity should contain valid data`() { val activity = Activity( - type = "WALKING", + type = ActivityType.WALKING, confidence = 100, ) - assertEquals("WALKING", activity.type) + assertEquals(ActivityType.WALKING, activity.type) assertEquals(100, activity.confidence) } @Test fun `Activity should contain valid confidence`() { - val activity = Activity(type = "WALKING", confidence = 80) + val activity = Activity(type = ActivityType.WALKING, confidence = 80) assertTrue(activity.confidence in 0..100) } } diff --git a/parser/src/test/kotlin/dev/hossain/timeline/model/SemanticTimelineTest.kt b/parser/src/test/kotlin/dev/hossain/timeline/model/SemanticTimelineTest.kt index 5962f89..51e3602 100644 --- a/parser/src/test/kotlin/dev/hossain/timeline/model/SemanticTimelineTest.kt +++ b/parser/src/test/kotlin/dev/hossain/timeline/model/SemanticTimelineTest.kt @@ -1,5 +1,6 @@ package dev.hossain.timeline.model +import ZonedDateTimeAdapter import com.google.common.truth.Truth.assertThat import com.squareup.moshi.Moshi import kotlin.test.Test @@ -8,7 +9,7 @@ import kotlin.test.Test * Test cases for [SemanticTimeline]. */ class SemanticTimelineTest { - private val moshi: Moshi = Moshi.Builder().build() + private val moshi: Moshi = Moshi.Builder().add(ZonedDateTimeAdapter()).build() @Test fun `given semantic timeline json should parse all timeline objects`() { @@ -44,8 +45,8 @@ class SemanticTimelineTest { assertThat(placeVisit.childVisits).isNull() assertThat(placeVisit.sectionId).isNull() - assertThat(placeVisit.duration.startTimestamp).isEqualTo("2021-08-01T01:22:24Z") - assertThat(placeVisit.duration.endTimestamp).isEqualTo("2021-08-01T01:25:36Z") + assertThat(placeVisit.duration.startTimestamp.toString()).isEqualTo("2021-08-01T01:22:24Z") + assertThat(placeVisit.duration.endTimestamp.toString()).isEqualTo("2021-08-01T01:25:36Z") assertThat(placeVisit.placeConfidence).isEqualTo("HIGH_CONFIDENCE") } diff --git a/parser/src/test/kotlin/dev/hossain/timeline/model/SettingsTest.kt b/parser/src/test/kotlin/dev/hossain/timeline/model/SettingsTest.kt index 96f76c8..2dc6e57 100644 --- a/parser/src/test/kotlin/dev/hossain/timeline/model/SettingsTest.kt +++ b/parser/src/test/kotlin/dev/hossain/timeline/model/SettingsTest.kt @@ -1,5 +1,6 @@ package dev.hossain.timeline.model +import ZonedDateTimeAdapter import com.google.common.truth.Truth.assertThat import com.squareup.moshi.Moshi import kotlin.test.Test @@ -8,7 +9,7 @@ import kotlin.test.Test * Test cases for [Settings]. */ class SettingsTest { - private val moshi: Moshi = Moshi.Builder().build() + private val moshi: Moshi = Moshi.Builder().add(ZonedDateTimeAdapter()).build() @Test fun `given settings json should parse all settings data`() {