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

FR: Add support for Swift object serialization in Cloud Functions #8528

Closed
EthanLozano opened this issue Aug 13, 2021 · 16 comments
Closed

FR: Add support for Swift object serialization in Cloud Functions #8528

EthanLozano opened this issue Aug 13, 2021 · 16 comments

Comments

@EthanLozano
Copy link

Please add Swift Codable support to Cloud Functions in a FirebaseFunctionsSwift module (or within FirebaseCombineSwift). This request is similar to #627 except for Cloud Functions rather than for Firestore.

More specifically, please add Encodable support when calling a function:

functions.httpsCallable("myCloudFunction").call(from: encodableObject)

Similarly, please add Decodable support to the function result:

functions.httpsCallable("myCloudFunction").call() { result, error in
    ...
    let obj = try result?.data(as: DecodableObject.self)
    ...
}
@paulb777
Copy link
Member

@EthanLozano Thanks for the Feature Request. My preference would be an implementation in a new FirebaseFunctionsSwift library comparable to the Codable implementation for Firestore and the Codable implementation in FirebaseDatabaseSwift added in #6568.

@mortenbekditlevsen
Copy link
Contributor

I just saw this issue referenced and wanted to chime in:
In my own project I'm using the exact same encoder/decoder for RTDB and functions.

Wrt code size I guess it would be silly to have yet another copy of encoder/decoder for so similar a purpose.

On the other hand it adds to the complexity of the project to split out the encoder / decoder into their own package.

Which solution would the Firebase team prefer? Another copy or trying to split up the project into another module?

@paulb777

@paulb777
Copy link
Member

@mortenbekditlevsen Good point! We definitely don't want to repeat code, so my preference would be to factor into another module. In Swift Package Manager, it could be a target, but not a product.

@mortenbekditlevsen
Copy link
Contributor

I'd like to help implementing that refactor, but I have a few questions:
Is cocoapods still a consideration here too? I'm not too skilled in the details of inter-pod dependencies...
Naming: I guess the shared encoder/decoder pair ought to have a more generic name than Database.Encoder and Database.Decoder.
Could a name like StructureEncoder and StructureDecoder be used and then make Database.Encoder and Database.Decoder be typealiases? The prefix: Structure is supposed to suggest that it encodes to 'structures' of arrays and dictionaries.

Note: A long time ago on the swift forums I've been suggesting that JSONEncoder and JSONDecoder from Foundation should support decoding both: Data (as today), String and 'structures' of dictionaries and arrays - since to me they are all JSON, just in various representations. There could easily be overloads for these extra representations. The response I've previously received is that the current implementation using JSONSerialization is an implementation detail, and perhaps a more performant decoding that could operate on streams of data could be implemented later on, and in that case they would be hesitant to commit to more APIs...

@peterfriese
Copy link
Contributor

Paging @schmidt-sebastian to get Firestore's perspective.

@mortenbekditlevsen
Copy link
Contributor

Quick comment regarding Firestore: I'd personally love to have the key coding strategies available here too. :-)

@paulb777
Copy link
Member

I like the Structure naming idea an interested to hear others' thoughts. @mortenbekditlevsen Will you send a link to the Swift forum discussion that you're referencing?

We still need CocoaPods, but we can defer the details. I could either provide guidance or set it up reasonably quickly once the SPM implementation is done.

@mortenbekditlevsen
Copy link
Contributor

Excellent, thanks for your comments.
The forum discussions got stranded - partly because I probably failed to push it forward quite enough:

https://forums.swift.org/t/idea-exposing-jsonencoder-and-jsondecoder-functionality/6379

https://forums.swift.org/t/revisiting-structureencoder-and-structuredecoder/12634

@mortenbekditlevsen
Copy link
Contributor

I created a PR that moves the encoder/decoder pair into a new target. Next I'll have a look at the requested functionality itself.

@mortenbekditlevsen
Copy link
Contributor

With regards to Firestore's encoding and decoding:
I am not 100% familiar with the specifics, but it appears to me that the main purpose of the encoder and decoder is to enable the decoding of 'values without keys' - in other words to support the DocumentID property wrapper.

I think that most of what is done in the FirestoreDecoder could in fact be moved out of the decoder and replaced by a concept, that is perhaps a bit more generic and would be ok to have in the StructureDecoder too.

If we create a protocol: Uncoded (or UnkeyedUncoded or something entirely different) that could be used to handle the special casing in:

  public func decode<T: Decodable>(_ type: T.Type, forKey key: Key) throws -> T {

(line 337 of FirestoreDecoder.swift)

I think that the actual logic of adding a DocumentReference to userInfo and getting that reference back in the decoding of DocumentID could be done outside of the decoder.

Another thing that happens in the FirestoreDecoder is the check for isFirestorePassthroughType.
I haven't looked at it very closely, but it seems like it might also be something that could be formalized as a protocol...
Or maybe just have:

    if let v = value as? T {
      return v
    }

instead of:

    if let v = value as? T {
      if isFirestorePassthroughType(v) {
        // All the native Firestore types that should not be encoded
        return (value as! T)
      }
    }

That would basically mean: If something in the encoded structure can be cast directly to the value that we are parsing, we just do so and return it.

It sounds to me like it would work, but of course it could be guarded by a protocol conformance check in order to play it safe...

Then there is the unboxing of Timestamp as Date. That could be implemented as a custom dateDecodingStrategy. That would not be 100% API stable, but the decoding that is performed in the Firestore overlay could at least opt-in to that date decoding strategy, so it would only be when you manually try and use the decoder that there could be an issue.

All in all I think there might be big gains to sharing encoder and decoder - and have the same 'strategies' available. And also keeping the encoder and decoder easily 'upgradable' when new functionality is added to JSONEncoder and JSONDecoder.

@mortenbekditlevsen
Copy link
Contributor

Another thought about FirestoreDecoder.swift:

Regarding the throwing that happens at line 349 when the key that is attempted to be decoded exists in the encoded structure.

This makes it very hard to evolve your model from either: going from an actual id property to a DocumentID property with the same name - or the other way around.
Such an evolution now basically requires a renaming of a property - and that may often go against coding style guidelines (if you have a fixed way of naming identifiers in your models).

I get that the intention is to avoid the confusion about where the value actually comes from, but otherwise in the decoding world we usually don't care about anything in the encoded structure that we are not using for decoding...

Should I raise this concern as a separate issue?

@mortenbekditlevsen
Copy link
Contributor

mortenbekditlevsen commented Oct 23, 2021

Now there's also also a PR (#8854) attempting to solve this actual issue.

@mortenbekditlevsen
Copy link
Contributor

And to explore merging the Firestore Encoder and Decoder into using StructureEncoder and StructureDecoder, I have created a PR (#8858) that adds two protocols to the encoder/decoder in FirebaseSharedSwift:
StructureCodingPassthroughType that will leave a type alone upon encoding and return the type directly upon decoding (if the type being decode matches the passthrough type).
StructureCodingUncodedUnkeyed that skips encoding and calls decoding without first trying to decode a value from the key.

I intentionally left out the check for presence of the key elsewhere in the encoded structure based on my comment about model evolution above.

I couldn't make FirebaseFirestoreSwift tests run, so I tested using a small test app.

All remaining 'work' that was before in Firestore.Encoder and Firestore.Decoder has been moved to FirebaseFirestoreSwift.
A custom dateDecodingStrategy called '.timestamp()' has been added to allow for decoding Timestamp as Date. But also this could be done in the FirebaseFirestoreSwift target to keep all knowledge about Firestore out of the actual encoder and decoder.

@paulb777
Copy link
Member

Yep the FirestoreSwift tests have a nested dependency on the C++ gtest framework which would be challenging to port to Swift Package Manager. It might be worth investigating if we could have pure Swift tests for the Firestore Swift tests. cc: @schmidt-sebastian @wu-hui

@schmidt-sebastian
Copy link
Contributor

I am not opposed to adding Swift only tests if this unblocks this effort.

@paulb777
Copy link
Member

Closing now that #8854 has merged to master

@paulb777 paulb777 added this to the 8.11.0 - M110 milestone Dec 30, 2021
@firebase firebase locked and limited conversation to collaborators Jan 29, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

6 participants