Skip to content

Commit

Permalink
dataconnect: LocalDate and LocalDateSerializer added (#6434)
Browse files Browse the repository at this point in the history
  • Loading branch information
dconeybe authored Nov 5, 2024
1 parent 93640ce commit 80019ca
Show file tree
Hide file tree
Showing 4 changed files with 290 additions and 0 deletions.
3 changes: 3 additions & 0 deletions firebase-dataconnect/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
errors related to using these experimental APIs.
([#6424](https://github.com/firebase/firebase-android-sdk/pull/6424)) and
([#6433](https://github.com/firebase/firebase-android-sdk/pull/6433))
* [changed] Replaced java.util.Date with
com.google.firebase.dataconnect.LocalDate.
([#6434](https://github.com/firebase/firebase-android-sdk/pull/6434))

# 16.0.0-beta02
* [changed] Updated protobuf dependency to `3.25.5` to fix
Expand Down
26 changes: 26 additions & 0 deletions firebase-dataconnect/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,24 @@ package com.google.firebase.dataconnect {
method public static void setLogLevel(@NonNull com.google.firebase.dataconnect.FirebaseDataConnect.Companion, @NonNull com.google.firebase.dataconnect.LogLevel);
}

@kotlinx.serialization.Serializable(with=LocalDateSerializer::class) public final class LocalDate {
ctor public LocalDate(int year, int month, int day);
method public int getDay();
method public int getMonth();
method public int getYear();
property public final int day;
property public final int month;
property public final int year;
}

public final class LocalDateKt {
method @NonNull public static com.google.firebase.dataconnect.LocalDate copy(@NonNull com.google.firebase.dataconnect.LocalDate, int year = year, int month = month, int day = day);
method @NonNull public static com.google.firebase.dataconnect.LocalDate toDataConnectLocalDate(@NonNull java.time.LocalDate);
method @NonNull public static com.google.firebase.dataconnect.LocalDate toDataConnectLocalDate(@NonNull kotlinx.datetime.LocalDate);
method @NonNull public static java.time.LocalDate toJavaLocalDate(@NonNull com.google.firebase.dataconnect.LocalDate);
method @NonNull public static kotlinx.datetime.LocalDate toKotlinxLocalDate(@NonNull com.google.firebase.dataconnect.LocalDate);
}

public enum LogLevel {
method @NonNull public static com.google.firebase.dataconnect.LogLevel valueOf(@NonNull String name) throws java.lang.IllegalArgumentException;
method @NonNull public static com.google.firebase.dataconnect.LogLevel[] values();
Expand Down Expand Up @@ -300,6 +318,14 @@ package com.google.firebase.dataconnect.serializers {
field @NonNull public static final com.google.firebase.dataconnect.serializers.DateSerializer INSTANCE;
}

public final class LocalDateSerializer implements kotlinx.serialization.KSerializer<com.google.firebase.dataconnect.LocalDate> {
method @NonNull public com.google.firebase.dataconnect.LocalDate deserialize(@NonNull kotlinx.serialization.encoding.Decoder decoder);
method @NonNull public kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
method public void serialize(@NonNull kotlinx.serialization.encoding.Encoder encoder, @NonNull com.google.firebase.dataconnect.LocalDate value);
property @NonNull public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
field @NonNull public static final com.google.firebase.dataconnect.serializers.LocalDateSerializer INSTANCE;
}

public final class TimestampSerializer implements kotlinx.serialization.KSerializer<com.google.firebase.Timestamp> {
method @NonNull public com.google.firebase.Timestamp deserialize(@NonNull kotlinx.serialization.encoding.Decoder decoder);
method @NonNull public kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.dataconnect

import android.annotation.SuppressLint
import com.google.firebase.dataconnect.serializers.LocalDateSerializer
import java.util.Objects
import kotlinx.serialization.Serializable

/**
* A date without a time-zone in the ISO-8601 calendar system, such as {@code 2007-12-03}. This is
* the default Kotlin type used to represent a `Date` GraphQL custom scalar in Firebase Data
* Connect.
*
* ### Description (adapted from [java.time.LocalDate])
*
* [LocalDate] is an immutable date-time object that represents a date, often viewed as
* year-month-day. For example, the value "2nd October 2007" can be stored in a [LocalDate].
*
* This class does not store or represent a time or time-zone. Instead, it is a description of the
* date, as used for birthdays. It cannot represent an instant on the time-line without additional
* information such as an offset or time-zone.
*
* ### Relationship to [java.time.LocalDate] and [kotlinx.datetime.LocalDate]
*
* This class exists solely to fill the gap for a "day-month-year" data type in Android API versions
* less than 26. When the Firebase Android SDK updates its `minSdkVersion` to 26 or later, then this
* class will be marked as "deprecated" and eventually removed.
*
* The [java.time.LocalDate] class was added in Android API 26 and should be used if it's available
* instead of this class. If [java.time.LocalDate] is available then [kotlinx.datetime.LocalDate] is
* a completely valid option as well, if it's desirable to take a dependency on
* https://github.com/Kotlin/kotlinx-datetime.
*
* Alternately, if your application has its `minSdkVersion` set to a value _less than_ 26, you can
* use "desugaring" (https://developer.android.com/studio/write/java8-support-table) to get access
* [java.time.LocalDate] class regardless of the API version used at runtime.
*
* ### Using [java.time.LocalDate] and [kotlinx.datetime.LocalDate] in code generation.
*
* By default, the Firebase Data Connect code generation will use this class when generating code
* for Kotlin. If, however, you want to use the preferable [java.time.LocalDate] or
* [kotlinx.datetime.LocalDate] classes, add a `dateClass` entry in your `connector.yaml` set to the
* fully-qualified class name that you'd like to use. For example,
*
* ```
* connectorId: demo
* authMode: PUBLIC
* generate:
* kotlinSdk:
* outputDir: ../../.generated/demo
* dateClass: java.time.LocalDate # or kotlinx.datetime.LocalDate
* ```
*
* ### Safe for Concurrent Use
*
* All methods and properties of [FirebaseDataConnect] are thread-safe and may be safely called
* and/or accessed concurrently from multiple threads and/or coroutines.
*
* @property year The year. The valid range is between 1583 and 9999, inclusive; however, this is
* _not_ checked or prevented by this class. Values less than 1583 are not forbidden; however, their
* interpretation by the Data Connect backend is unspecified. See
* https://en.wikipedia.org/wiki/ISO_8601#Years for more details.
* @property month The month. The valid range is between 1 and 12, inclusive; however, this is _not_
* checked or prevented by this class.
* @property day The day of the month. The valid range is between 1 and 31, inclusive; however, this
* is _not_ checked or prevented by this class.
*/
@Serializable(with = LocalDateSerializer::class)
public class LocalDate(public val year: Int, public val month: Int, public val day: Int) {

/**
* Compares this object with another object for equality.
*
* @param other The object to compare to this for equality.
* @return true if, and only if, the other object is an instance of [LocalDate] and has the same
* values for [year], [month], and [day] as this object, respectively.
*/
override fun equals(other: Any?): Boolean =
other is LocalDate && other.year == year && other.month == month && other.day == day

/**
* Calculates and returns the hash code for this object.
*
* The hash code is _not_ guaranteed to be stable across application restarts.
*
* @return the hash code for this object, that incorporates the values of this object's public
* properties.
*/
override fun hashCode(): Int = Objects.hash(LocalDate::class, year, month, day)

/**
* Returns a string representation of this object, useful for debugging.
*
* The string representation is _not_ guaranteed to be stable and may change without notice at any
* time. Therefore, the only recommended usage of the returned string is debugging and/or logging.
* Namely, parsing the returned string or storing the returned string in non-volatile storage
* should generally be avoided in order to be robust in case that the string representation
* changes.
*
* @return a string representation of this object, which includes the class name and the values of
* all public properties.
*/
override fun toString(): String = "LocalDate(year=$year, month=$month, day=$day)"
}

/**
* Creates and returns a [java.time.LocalDate] object that represents the same date as this object.
*
* Be sure to _only_ call this method if [java.time.LocalDate] is available; otherwise the behavior
* is undefined. If your application's `minSdkVersion` is greater than or equal to `26`, or if you
* have configured "desugaring" (https://developer.android.com/studio/write/java8-support-table)
* then it is guaranteed to be available. Otherwise, check [android.os.Build.VERSION.SDK_INT] at
* runtime and verify that its value is at least [android.os.Build.VERSION_CODES.O] before calling
* this method.
*
* @see java.time.LocalDate.toDataConnectLocalDate
* @see kotlinx.datetime.LocalDate.toDataConnectLocalDate
* @see toKotlinxLocalDate
*/
@SuppressLint("NewApi")
public fun LocalDate.toJavaLocalDate(): java.time.LocalDate =
java.time.LocalDate.of(year, month, day)

/**
* Creates and returns a [LocalDate] object that represents the same date as this
* [java.time.LocalDate] object. This is the inverse operation of [LocalDate.toJavaLocalDate].
*
* Be sure to _only_ call this method if [java.time.LocalDate] is available. See the documentation
* for [LocalDate.toJavaLocalDate] for details.
*
* @see toJavaLocalDate
* @see toKotlinxLocalDate
* @see kotlinx.datetime.LocalDate.toDataConnectLocalDate
*/
@SuppressLint("NewApi")
public fun java.time.LocalDate.toDataConnectLocalDate(): LocalDate =
LocalDate(year = year, month = monthValue, day = dayOfMonth)

/**
* Creates and returns a [kotlinx.datetime.LocalDate] object that represents the same date as this
* object.
*
* Be sure to _only_ call this method if your application has a dependency on
* `org.jetbrains.kotlinx:kotlinx-datetime`; otherwise, the behavior of this method is undefined. If
* your `minSdkVersion` is less than `26` then you _may_ also need to configure "desugaring"
* (https://developer.android.com/studio/write/java8-support-table).
*
* @see kotlinx.datetime.LocalDate.toDataConnectLocalDate
* @see java.time.LocalDate.toDataConnectLocalDate
* @see toJavaLocalDate
*/
public fun LocalDate.toKotlinxLocalDate(): kotlinx.datetime.LocalDate =
kotlinx.datetime.LocalDate(year = year, monthNumber = month, dayOfMonth = day)

/**
* Creates and returns a [LocalDate] object that represents the same date as the given
* [kotlinx.datetime.LocalDate] object. This is the inverse operation of [toKotlinxLocalDate].
*
* Be sure to _only_ call this method if your application has a dependency on
* `org.jetbrains.kotlinx:kotlinx-datetime`. See the documentation for [toKotlinxLocalDate] for
* details.
*
* @see toKotlinxLocalDate
* @see toJavaLocalDate
* @see java.time.LocalDate.toDataConnectLocalDate
*/
public fun kotlinx.datetime.LocalDate.toDataConnectLocalDate(): LocalDate =
LocalDate(year = year, month = monthNumber, day = dayOfMonth)

/** Creates and returns a new [LocalDate] instance with the given property values. */
public fun LocalDate.copy(
year: Int = this.year,
month: Int = this.month,
day: Int = this.day,
): LocalDate = LocalDate(year = year, month = month, day = day)
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.dataconnect.serializers

import com.google.firebase.dataconnect.LocalDate
import java.util.regex.Matcher
import java.util.regex.Pattern
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

/**
* An implementation of [KSerializer] for serializing and deserializing [LocalDate] objects in the
* wire format expected by the Firebase Data Connect backend.
*/
public object LocalDateSerializer : KSerializer<LocalDate> {

override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("com.google.firebase.dataconnect.LocalDate", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: LocalDate) {
value.run {
require(year >= 0) { "invalid value: $value (year must be non-negative)" }
require(month >= 0) { "invalid value: $value (month must be non-negative)" }
require(day >= 0) { "invalid value: $value (day must be non-negative)" }
}
val serializedDate =
"${value.year}".padStart(4, '0') +
'-' +
"${value.month}".padStart(2, '0') +
'-' +
"${value.day}".padStart(2, '0')
encoder.encodeString(serializedDate)
}

override fun deserialize(decoder: Decoder): LocalDate {
val decodedString = decoder.decodeString()
val matcher = Pattern.compile("^(\\d+)-(\\d+)-(\\d+)$").matcher(decodedString)
require(matcher.matches()) {
"date \"$decodedString\" does not match regular expression: ${matcher.pattern()}"
}

fun Matcher.groupToIntIgnoringLeadingZeroes(index: Int): Int {
val groupText = group(index)!!.trimStart('0')
return if (groupText.isEmpty()) 0 else groupText.toInt()
}

val year = matcher.groupToIntIgnoringLeadingZeroes(1)
val month = matcher.groupToIntIgnoringLeadingZeroes(2)
val day = matcher.groupToIntIgnoringLeadingZeroes(3)

return LocalDate(year = year, month = month, day = day)
}
}

0 comments on commit 80019ca

Please sign in to comment.