Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dataconnect: LocalDate and LocalDateSerializer added #6434

Merged
merged 2 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}
Loading