-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Codable firestore #838
Codable firestore #838
Conversation
* Move Example/Firestore_SwiftTests_iOS to Swift/Tests * Delete generated test case * Update Info.plist location to match
Also corresponding xcodeproj changes
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). 📝 Please visit https://cla.developers.google.com/ to sign. Once you've signed, please reply here (e.g.
|
I signed it! |
CLAs look good, thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've taken a pass through the decoder and note that there is a ton of effectively duplicate code. It's very hard to tell what's unique about each overload in here.
I've made some suggestions for how to cut down on this.
@@ -0,0 +1,38 @@ | |||
// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All files need a copyright notice to be in this repo. You can copy from the top of any existing file and adjust the date or use this:
/*
* Copyright 2018 Google
*
* 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.
*/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
import FirebaseFirestore | ||
|
||
enum FirestoreDecodingError: Error { | ||
case decodingIsNotSupported |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Google's coding style requires two space indents. Rather than fix this by hand please wait for PR #847 to land and run that here to fix this.
import FirebaseFirestore | ||
|
||
/** | ||
* A protocol describing the encodable properties of a FirebaseFirestore. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the encodable properties of a FieldValue, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
@@ -0,0 +1,38 @@ | |||
// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just re-read our internal style guide and realized that these files should really be Class+Protocol.swift. In this case DocumentReference+Codable.swift
. Sorry to lead you astray with the other PR :-(.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
|
||
import FirebaseFirestore | ||
|
||
enum FirestoreDecodingError: Error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These don't have much to do with DocumentReference. Perhaps put these in an Errors.swift
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: decoder.codingPath + [_FirestoreKey(index: currentIndex)], debugDescription: "Unkeyed container is at end.")) | ||
} | ||
|
||
decoder.codingPath.append(_FirestoreKey(index: currentIndex)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this (and the defer) were at the top of function then we wouldn't have to append to to the codingPath to compose the error message for the !isAtEnd case.
defer { decoder.codingPath.removeLast() } | ||
|
||
guard let decoded = try decoder.unbox(container[currentIndex], as: Bool.self) else { | ||
throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: decoder.codingPath + [_FirestoreKey(index: currentIndex)], debugDescription: "Expected \(type) but found null instead.")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also decoder.codingPath
already has _FirestoreKey(index: currentIndex)
appended to the end of it; this will duplicate the index in the coding path.
guard !self.isAtEnd else { | ||
throw DecodingError.valueNotFound(KeyedDecodingContainer<NestedKey>.self, | ||
DecodingError.Context(codingPath: self.codingPath, | ||
debugDescription: "Cannot get nested keyed container -- unkeyed container is at end.")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Despite the different error message, this could probably also use expectNotAtEnd
. I'm not sure there's any special value in giving a slightly better error message for cases that seem extremely unlikely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
} | ||
|
||
let value = self.container[self.currentIndex] | ||
guard !(value is NSNull) else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could use requireValue
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We cannot really use requireValue
here as it expects the Optional
and we are checking for the NSNull
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
debugDescription: "Cannot get keyed decoding container -- found null value instead.")) | ||
} | ||
|
||
guard let dictionary = value as? [String : Any] else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This through the bit that constructs the KeyedDecodingContainer
seems as if it can be a method we factor out on the top-level decoder.
public func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> {
return try container(of: storage.topContainer, keyedBy: type)
}
public func container<Key>(of value: Any, keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> {
guard !(value is NSNull) else {
throw DecodingError.valueNotFound(KeyedDecodingContainer<Key>.self,
DecodingError.Context(codingPath: codingPath,
debugDescription: "Cannot get keyed decoding container -- found null value instead."))
}
guard let dictionary = value as? [String: Any] else {
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Not a dictionary")
throw DecodingError.typeMismatch([String: Any].self, context)
}
let container = _FirestoreKeyedDecodingContainer<Key>(referencing: self, wrapping: dictionary)
return KeyedDecodingContainer(container)
}
So there's good news and bad news. 👍 The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so. Everything is all good there. 😕 The bad news is that it appears that one or more commits were authored by someone other than the pull request submitter. We need to confirm that all authors are ok with their commits being contributed to this project. Please have them confirm that here in the pull request. Note to project maintainer: This is a terminal state, meaning the |
@wilhuff I merged master into this branch... And now I am not sure what should I do :) Should I create another PR against master? |
Just keep going. If you're really concerned about the clarity of the PR
stack, feel free to rebase your changes and force push to your repository.
We're going to squash merge your change into canonical history so it
doesn't matter too much if your changes are all that clear at the commit
level.
…On Tue, Feb 27, 2018 at 7:44 AM Oleksii Dykan ***@***.***> wrote:
@wilhuff <https://github.com/wilhuff> I merged master into this branch...
And now I am not sure what should I do :) Should I create another PR
against master?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#838 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AJxjkPzXejXuGEt0sJQM_mbBBiYa9jahks5tZCLagaJpZM4SPic9>
.
|
The base PR from which this started has since been merged to master. I've retargeted this PR against master and now the diff is clean again. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Decoder changes are great. I have minor naming feedback (see specific callouts) and a larger question about how to handle values beyond the range of the intended storage type. JSONDecoder seems to want to fail, but in other context Firestore just does what a native cast would do.
@@ -49,6 +49,16 @@ | |||
ReferencedContainer = "container:Firestore.xcodeproj"> | |||
</BuildableReference> | |||
</TestableReference> | |||
<TestableReference |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The base state from which you branched was broken. I've since fixed this, but unfortunately these diffs remain. For all the .xcscheme
files, make sure your upstream branch is up-to-date and then git checkout the master version of these files and commit them. After pushing you shouldn't see any additions to project files.
@@ -0,0 +1,16 @@ | |||
// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copyright notice.
I realize this isn't super important to you, but this is the only way that Google allows me to continue to work in the open with you, so it's very important to me :-).
} | ||
|
||
@available(swift 4.0.0) | ||
fileprivate class _FirestoreDecoder : Decoder { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is Firestore.DocumentDecoder
a possibility? I'm still not clear on exactly what the nested struct within Firestore
is for. To me this seems like it should be the public interface on the extension of DocumentSnapshot
.
That is to say DocumentDecoder
becomes internal
, and we create an extension of DocumentSnapshot
that uses it (as above) to implement the public API for decoding to an object of a Decodable
type.
} | ||
|
||
@available(swift 4.0.0) | ||
fileprivate struct _FirestoreDecodingStorage { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be named DocumentDecodingStorage
?
My basic concern is that "Firestore decoding" is too general. For example, we could, in the future decode whole query snapshots, in which case a "Firestore decoder" is insufficient.
} | ||
|
||
@available(swift 4.0.0) | ||
fileprivate struct _FirestoreKeyedDecodingContainer<K : CodingKey> : KeyedDecodingContainerProtocol { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DocumentKeyedDecodingContainer
?
} | ||
|
||
public func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { | ||
let entry = try require(key: key) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks substantially better. Thank you for working through it.
} | ||
|
||
@available(swift 4.0.0) | ||
fileprivate struct _FirestoreUnkeyedDecodingContainer : UnkeyedDecodingContainer { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DocumentUnkeyedDecodingContainer
?
return false | ||
} | ||
|
||
/* FIXME: If swift-corelibs-foundation doesn't change to use NSNumber, this code path will need to be included and tested: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How are these FIXMEs to be addressed? I assume it comes from the implementation to which you referred.
Do you think there a way to add a check to make sure that we're not messing this up? I'm more concerned with correctness than speed so an extra else case here (after the two cases that are realistically possible) doesn't bother me.
|
||
let int8 = number.int8Value | ||
guard NSNumber(value: int8) == number else { | ||
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type).")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Parsed JSON" no longer applies. Maybe "Firestore Datasnapshot"?
// * If it was a Float, you will get back the precise value | ||
// * If it was a Double or Decimal, you will get back the nearest approximation if it will fit | ||
let double = number.doubleValue | ||
guard abs(double) <= Double(Float.greatestFiniteMagnitude) else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- @mikelehen What's the right way to respond to this based on our Android implementation?
It seems like CustomClassMapper just relies on "cast" operations (Number#floatValue
): non-failing, saturating casts that would have e.g. positive values beyond ~3.4 x 10^38 saturate to Infinity when assigned to a model value containing a Float instead of a double. Isn't a similar response appropriate here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general the encoder stuff is far less egregious and doesn't seem like it needs much work (aside from naming).
} | ||
|
||
@available(swift 4.0.0) | ||
fileprivate class _FirestoreEncoder : Encoder { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The names in here should parallel the resolution on the decoder (i.e. Document*)
Any progress on this? Would be great to have this part of the SDK. |
Thanks for the hard work @alickbass @wilhuff! |
A great work @alickbass @wilhuff. Thank you both. |
We really need this for our iOS project, so please please keep up the good work @alickbass @wilhuff! |
Hi guys! What's the progress on this beautiful and highly anticipated feature? |
Any update on this? |
Closing and continuing in #2178 |
Hi guys! So this is a WIP PR to solve #627. It is based on #815.
In general, code is almost 1-to-1 port of JSONEncoder from Foundation and PlistEncoder with some minor changes to fit it to Firestore. Most of the code is just boilerplate and the most interesting part is in
in
FirestoreDecoder
andin
FirestoreEncoder
.I also created internal types
Firestore.Encoder
andFirestore.Decoder
to able to test without needingDocumentReference
andDocumentSnapshot
. Everything is internal now without any public methods, so that we can already start testing and deciding if it's the right approach without the pain of approving the public API//сс @wilhuff @paulb777