Skip to content

JSONEncoder implementation #1048

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

Merged
merged 6 commits into from
Jun 26, 2017
Merged

JSONEncoder implementation #1048

merged 6 commits into from
Jun 26, 2017

Conversation

bubski
Copy link
Contributor

@bubski bubski commented Jun 18, 2017

Resolves: https://bugs.swift.org/browse/SR-5195

This PR mirrors JSONEncoder implementation from SDK for Darwin, so that it's available on Linux.
swiftlang/swift#9005

platformConsistentCast

There are two major discrepancies between macOS and Linux.

  1. On macOS JSONSerialization uses only NSNumber to represent numbers in json objects. On Linux however, standard Swift types like Double or Int are used.
  2. Casting between NSNumber and Swift numeric types behaves very differently on macOS and Linux.

To keep matters consistent, I use func platformConsistentCast in JSONEncoder.swift instead of as until the above issues are resolved.

New swift compiler flag

-DTEST_TARGET
This flag is used when compiling TestFoundation sources.

Because @testable doesn't work on Linux, I had to add ConsistentCasting.swift to both Foundation and TestFoundation targets, to expose internal func platformConsistentCast for tests.

-DTEST_TARGET is used to only import Foundation in ConsistentCasting.swift if it's compiled as a part of TestFoundation target.

Mirrored files (with slight changes to resolve compilation errors on Linux):

JSONEncoder.swift from:
https://github.com/apple/swift/blob/c5ff1f2cac8da6a14330f4b033b94c7c926d2126/stdlib/public/SDK/Foundation/JSONEncoder.swift

Codable.swift from:
https://github.com/apple/swift/blob/012ea9373b5d2f72c59e16e18bd077c3ee86c745/stdlib/public/SDK/Foundation/Codable.swift

Data.swift (Data : Codable implementation) from:
https://github.com/apple/swift/blob/c40ba96328129d322a41afbd018b2a4081409e5e/stdlib/public/SDK/Foundation/Data.swift#L1759

Date.swift (Date : Codable implementation) from:
https://github.com/apple/swift/blob/012ea9373b5d2f72c59e16e18bd077c3ee86c745/stdlib/public/SDK/Foundation/Date.swift#L288

URL.swift (URL: Codable implementation) from:
https://github.com/apple/swift/blob/c5ff1f2cac8da6a14330f4b033b94c7c926d2126/stdlib/public/SDK/Foundation/URL.swift#L1210

NSError.swift (new error codes) from:
https://github.com/apple/swift/pull/9005/files#diff-a25f757c0d5c775e5b48e180b4a24efa

@bubski
Copy link
Contributor Author

bubski commented Jun 19, 2017

@swift-ci please test

@parkera parkera requested a review from itaiferber June 19, 2017 16:38
@parkera
Copy link
Contributor

parkera commented Jun 19, 2017

Thanks for taking this on!

@itaiferber itaiferber requested a review from phausler June 19, 2017 16:45
@itaiferber
Copy link
Contributor

I'll be able to review this more in depth later today — thanks for bringing this over to Linux! At first glance, looks reasonable to me.

@itaiferber
Copy link
Contributor

@phausler What do you think about the numeric casting abstractions here?

@@ -2778,7 +2808,7 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
LIBRARY_SEARCH_PATHS = "$(inherited)";
MACH_O_TYPE = mh_execute;
OTHER_SWIFT_FLAGS = "-DDEPLOYMENT_ENABLE_LIBDISPATCH -swift-version 3";
OTHER_SWIFT_FLAGS = "-DDEPLOYMENT_ENABLE_LIBDISPATCH -swift-version 3 -DTEST_TARGET";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to add this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that we import Foundation/import SwiftFoundation here
https://github.com/apple/swift-corelibs-foundation/pull/1048/files#diff-fa28084658370ec9069bda50577d8ab1R14
only when build for TestFoundation

platformConsistentCast is internal, I cannot expose it to TestFoundation, so needed to add this source file to TestFoundation target.

return nil
}

protocol _PlatformConsistentCasting {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be encompassed in the Bridging.swift, also protocols are a bit slippery for scope - we should annotate this as internal

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@phausler could you say a bit more why protocols are slippery for scope?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have had issues where declarations accidentally leak out beyond the module of Foundation (however I haven't seen that since).

Even though internal is the default application of scope, it just seems to be good hygiene to make certain they are marked as such.

}
}

extension Int8: _PlatformConsistentCasting {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should pull the implementation from NSNumber.swift from the overlay to make it have parity

@phausler
Copy link
Contributor

Overall this looks like the right direction, however I think we should try and figure out a consistent way of implementing the casting via using the exactly or the bridge methods.

I don't think it is out of scope to potentially change the JSONEncoder/JSONDecoder on Darwin to match if we can figure out a good way of implementing it so that we have parity for numeric types.

@bubski
Copy link
Contributor Author

bubski commented Jun 19, 2017

@phausler Thanks for the review.
I'll have time later today to take a deeper look into NSNumber.swift and Bridging.swift.

@phausler
Copy link
Contributor

let me know if you need any help; I think we can accomplish this pretty easily via the _SwiftValue.fetch method which simulates as? casts and make this work for not just JSONEncoder but have it work better for all numeric bridges.

@bubski
Copy link
Contributor Author

bubski commented Jun 19, 2017

I can't really see how casting would benefit from using _SwiftValue.fetch.
Could you explain a bit more @phausler?

From what I see, _SwiftValue does rather, so to say, bridging than casting, between Swift and NS types.

Also, _SwiftValue.fetch returns Any. The way I see it, a generic function would be suited to do casting.

And, if I'm not mistaken, _SwiftValue revolves around _StructBridgeable and _ObjectBridgeable, which describe one-to-one bridging, but for casting we need to be able to have multiple output types, e.g. NSNumber can be casted to not only Int but also UInt, Double, Int8 etc.

I'd appreciate any guidance here.

@phausler
Copy link
Contributor

So the problem is going to be recursive type conversions; e.g. Dictionaries that contain other collections. We will always have some mismatch of parity of some of the elements but I think we can get a 90% use case solution here with _SwiftValue.fetch.

Here is the idea:
We add a new flag to JSONSerialization to produce reference types instead of value types.
That will make the parser emit reference semantics and contain NSNumbers, NSStrings, NSDictionaries etc (which all of those types exist today in swift-corelibs-foundation as either simple wrappers like NSString as a wrapper for String, or CF types like NSNumber and CFNumber).

Since the numeric type accessors have type (bound to the bridgeable type) being sent in we can construct the requested type via the bridge method _conditionallyBridgeFromObjectiveC etc. That way when client code requests a Double or an Int8 they get the expected behavior.

This also means that we need to bring across the updated NSNumber.swift bridge in the overlay (which the entire intent of changing how that was bridged was done so specifically for Codable support)

The long-tail advantage here is that we can likely get better behavior out of JSONSerialization or PropertyListSerialization for developers by going down this route; ideally we should share as much code and behavior between the Darwin versions and the swift-corelibs version as possible.

If it would help I can knock out a rough draft of bringing the NSNumber changes over. I think that going down that route will aide ensuring the JSONEncoder to be as close as possible.

@phausler
Copy link
Contributor

@parkera What is your weigh in on this? I know we have limited working schedule for things but I think it is pretty important that we get this right.

@bubski
Copy link
Contributor Author

bubski commented Jun 20, 2017

@phausler right, recursion would break…

As for JSONSerialization, I would expect it to be aligned with Darwin's one by default. In the end, it is supposed to be NSJSONSerialization mirror, so it should return the same data types, don't you think?
My thinking is, returning reference types should be opt-out rather than opt-in, if there is to be such a switch at all.
Am I wrong?

@phausler could you help me out and explain what overlay means?
I'm only now getting familiar and up to speed with the project, so forgive my basic questions.

@parkera
Copy link
Contributor

parkera commented Jun 20, 2017

@bubski I discussed this with @phausler some more and we had an idea which I think will simplify this PR quite a bit by removing the need for the casting. We can add an internal-only entry point to JSONSerialization to decode with an option set to prefer NSNumber output instead of Int, Float, Bool, and friends.

Then, when the encoder/decoder would have called platformConsistentCast it can instead still use as?, because the type returned from decoding is actually an NSNumber - and this cast will work across platforms.

The reason we decided to return the Swift numeric types from JSONSerialization in the first place was because of this same issue, that casting didn't work the same. We thought people would usually want the Swift types and not the Foundation types, so we were trying to remove the need to do some kind of Linux-specific as cast.

Taking a quick look at the JSON decoder, I think this option would be as simple as passing another option through to the parseValue function and using it to return NSNumber(value: whatever) instead of the built-in bool types and Swift numerics.

Hopefully this makes sense. Please let me know if you have any questions.

@bubski
Copy link
Contributor Author

bubski commented Jun 20, 2017

@parkera Yes, this makes sense and sounds like a plan 👍 I'll get around to it.
And thanks for the story behind types in JSONSerialization.

Is casting to/from Foundation types on Linux considered? If there's any useful reading around that subject that you know, please share.

@parkera
Copy link
Contributor

parkera commented Jun 21, 2017

I'd love to support using as to "bridge-cast", but not everyone agrees that this functionality should be available on Linux. In my mind, it's a key part of the cross-platform source compatibility story. But in any case, it's not happening in the short term so we should assume it won't exist.

@bubski
Copy link
Contributor Author

bubski commented Jun 23, 2017

@parkera Followed your advise and pushed changes.
cc: @phausler @itaiferber

@@ -23,6 +23,7 @@ extension JSONSerialization {
public static let mutableContainers = ReadingOptions(rawValue: 1 << 0)
public static let mutableLeaves = ReadingOptions(rawValue: 1 << 1)
public static let allowFragments = ReadingOptions(rawValue: 1 << 2)
internal static let useReferenceNumericTypes = ReadingOptions(rawValue: 1 << 3)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it might be a good idea to use some other value than 1 << 3 since it would be quite likely that we could add a public value that uses that definition.

my suggestion is use a higher value...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How high would you recommend?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like 1 << 12 or 1 << 15 should suffice

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're unlikely to need to add that many options; in the future, we can modify this value if need be, since it's just internal here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌 done

bubski added 2 commits June 23, 2017 19:01
…nto json-encoder

# Conflicts:
#	Foundation.xcodeproj/project.pbxproj
#	Foundation/Data.swift
@phausler
Copy link
Contributor

As a follow up task; I know Itai is working on some other additional changes to the encoders. We should figure out a way to be able to copy/paste the code easily between the two implementations. That way when changes are made in one location we can just do a simple duplication instead of needing to re-port the changes over again.

@bubski
Copy link
Contributor Author

bubski commented Jun 26, 2017

@itaiferber @phausler should I make any more changes in this PR?

@phausler
Copy link
Contributor

I think it looks good as a starting point, lets revisit the follow up task once Itai lands some of the other updates.

I will let Itai do the final review and merge.

Copy link
Contributor

@itaiferber itaiferber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM overall! We'll probably want to start porting over some Codable implementations from other Foundation types (swiftlang/swift#9715, swiftlang/swift#10404).

Also, I'm not sure how JSONSerialization handles Decimal if at all, but we'll probably need to apply swiftlang/swift#10553 here too.

@phausler Good to merge, though; these changes can come in future PRs.


// Adding the following extensions to EncodingError and DecodingError allows them to bridge to NSErrors implicitly.

fileprivate let NSCodingPathErrorKey = "NSCodingPath"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we don't have error bridging in Linux, I think this whole section (short of the helper methods on DecodingError) can be removed.

@bubski
Copy link
Contributor Author

bubski commented Jun 26, 2017

Thanks @itaiferber and @phausler.

I'll continue working around the things you mentioned in coming PRs.

@parkera
Copy link
Contributor

parkera commented Jun 26, 2017

@swift-ci test and merge

@swift-ci swift-ci merged commit d54fcb9 into swiftlang:master Jun 26, 2017
@ianpartridge
Copy link
Contributor

Hi @bubski - thank you so much for working on this! Do you have any news on adding Codable conformances to the other Foundation types?

@bubski
Copy link
Contributor Author

bubski commented Jul 6, 2017

@ianpartridge credit goes to @itaiferber . He's done all the heavy lifting.

Yes, I'm porting other types to Foundation, but hit some bugs which blocked me.

Here's one: #1092
I couldn't test CharacterSet : Codable properly without isEqual working, so I need that to be merged first.

My approach in #1092 might not be the best though. I know that @phausler is reworking some NSCF stuff which might solve this problem as well.

@ianpartridge
Copy link
Contributor

Sounds great - glad to hear you're making progress. Is there a public branch you're pushing to, so I can follow along?

@bubski
Copy link
Contributor Author

bubski commented Jul 6, 2017

Sure @ianpartridge https://github.com/bubski/swift-corelibs-foundation/tree/codable
But be warned, it's all WIP and messy :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants