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

Add a syntactic "Add Codable structs from JSON" code action #1205

Merged
merged 3 commits into from May 7, 2024

Conversation

DougGregor
Copy link
Member

@DougGregor DougGregor commented Apr 26, 2024

Add a syntactic action that takes JSON pasted into a Swift file or
placed in a string literal, then turns it into a set of Codable
structs that can represent the JSON. Our typical example starts like
this:

{
    "name": "Produce",
    "shelves": [
        {
            "name": "Discount Produce",
            "product": {
                "name": "Banana",
                "points": 200,
                "description": "A banana that's perfectly ripe."
            }
        }
    ]
}

and turns into this:

struct JSONValue: Codable {
    var name: String
    var shelves: [Shelves]

    struct Shelves: Codable {
        var name: String
        var product: Product

        struct Product: Codable {
            var description: String
            var name: String
            var points: Double
        }
    }
}

The refactoring itself would live down in the swift-syntax package if
not for its dependency on Foundation. We'll move as appropriate.

@DougGregor
Copy link
Member Author

@swift-ci please test

3 similar comments
@DougGregor
Copy link
Member Author

@swift-ci please test

@DougGregor
Copy link
Member Author

@swift-ci please test

@DougGregor
Copy link
Member Author

@swift-ci please test

@DougGregor
Copy link
Member Author

@swift-ci please test Windows

1 similar comment
@DougGregor
Copy link
Member Author

@swift-ci please test Windows

@DougGregor
Copy link
Member Author

@swift-ci please test Linux

1 similar comment
@DougGregor
Copy link
Member Author

@swift-ci please test Linux

Copy link
Collaborator

@ahoppen ahoppen left a comment

Choose a reason for hiding this comment

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

Copied a few review comments from #1169 😉

Comment on lines 113 to 124
}

extension ConvertJSONToCodableStruct {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let’s just shove them all together instead of having extensions of the same type within the same file.

Suggested change
}
extension ConvertJSONToCodableStruct {

Same once more a few lines below.

Copy link
Member Author

Choose a reason for hiding this comment

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

I want the protocol-conforming extensions separate, but other than that---sure

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is no longer a protocol-conforming extension 😜 Also, I’m not generally a fan of putting protocol conformances in extensions. I think it makes it harder to reason about which protocols a type conforms to.

}

extension Array where Element == Any {
fileprivate func unwrap(_ depth: Int) -> Dictionary<String, Any>? {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you add a comment of what unwrap does?

let uri = DocumentURI.for(.swift)
let positions = testClient.openDocument(
"""
1️⃣{
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nitpick

Suggested change
1️⃣{
1️⃣{

Also, 2 spaces indentation would be nice here for consistency.

Comment on lines 203 to 305
public func assertStringsEqualWithDiff(
_ actual: String,
_ expected: String,
_ message: String = "",
additionalInfo: @autoclosure () -> String? = nil,
file: StaticString = #filePath,
line: UInt = #line
) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

If we’re adding the string diff functions, could you put them in SKTestSupport so that other tests can also use them?


// Use `CollectionDifference` on supported platforms to get `diff`-like line-based output. On
// older platforms, fall back to simple string comparison.
if #available(macOS 10.15, *) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The #available check isn’t needed since sourcekit-lsp targets macOS 13.

Add a syntactic action that takes JSON pasted into a Swift file or
placed in a string literal, then turns it into a set of Codable
structs that can represent the JSON. Our typical example starts like
this:

```
{
    "name": "Produce",
    "shelves": [
        {
            "name": "Discount Produce",
            "product": {
                "name": "Banana",
                "points": 200,
                "description": "A banana that's perfectly ripe."
            }
        }
    ]
}
```

and turns into this:

```swift
struct JSONValue: Codable {
    var name: String
    var shelves: [Shelves]

    struct Shelves: Codable {
        var name: String
        var product: Product

        struct Product: Codable {
            var description: String
            var name: String
            var points: Double
        }
    }
}
```

When converting to JSON, we attempt to reason about multiple JSON
objects on the same level to detect when there are optional fields,
due to either an explicit null or due to the absence of fields in some
of the JSON objects that are conceptually stored together.

The refactoring itself would live down in the swift-syntax package if
not for its dependency on Foundation. We'll move it when appropriate.
@DougGregor
Copy link
Member Author

@swift-ci please test

@DougGregor
Copy link
Member Author

@swift-ci please test Windows

@DougGregor
Copy link
Member Author

I just went ahead and rewrote this with a completely new structure that does merging of like elements in the whole tree. When merging a field w/ a missing value (or one explicitly marked null), we introduce an optional in Swift. This also handles Boolean values specially.

@DougGregor
Copy link
Member Author

@swift-ci please test

@DougGregor
Copy link
Member Author

@swift-ci please test Windows

Copy link
Collaborator

@ahoppen ahoppen left a comment

Choose a reason for hiding this comment

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

That’s a lot shorter and simpler and even implements merging of structs 🤩

I have a few, mostly nitpicky, comments.

"""
1️⃣{
"name": "Produce",
"shelves": [
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think right now it’s only possible to invoke the refactoring from the opening {. I think it would be a lot more discoverable if you could invoke it from anywhere within the JSON.

Copy link
Member Author

Choose a reason for hiding this comment

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

No, the refactoring can be invoked from elsewhere in the closure. The test harness doesn't make it easy to check for places other than the start, though.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, I see what's going on. The parser isn't treating the whole blob as a single closure, so we sometimes end up in the "unexpected" area. I don't have a great fix at the moment.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you file an issue for that?

Comment on lines 113 to 124
}

extension ConvertJSONToCodableStruct {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is no longer a protocol-conforming extension 😜 Also, I’m not generally a fan of putting protocol conformances in extensions. I think it makes it harder to reason about which protocols a type conforms to.

init(value: Any) {
switch value {
case let string as String:
if string == "true" || string == "false" {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Interesting that JSONSerialization doesn’t deserialize those to an NSNumber. That does make me wonder: Does JSONSerialization deserialize "null" as NSNull or also as a string with value null? I don’t think we’ve got a test for that.

Copy link
Member Author

Choose a reason for hiding this comment

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

It leaves them all as strings. A "null" is a dreadful thing because it messes with the merging logic.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there any case that does produce an NSNull then?

Comment on lines 234 to 240
// Print any nested types.
for (typeName, object) in nestedTypes {
MemberBlockItemSyntax(
leadingTrivia: (typeName == nestedTypes.first?.0) ? .newlines(2) : .newline,
decl: object.asDeclSyntax(name: typeName)
)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I know this is a subjective opinion, but I would always put the nested types before the members.

@DougGregor
Copy link
Member Author

@swift-ci please test

@DougGregor
Copy link
Member Author

@swift-ci please test Windows

@DougGregor DougGregor enabled auto-merge May 7, 2024 00:58
@DougGregor DougGregor merged commit 7e5f87f into apple:main May 7, 2024
3 checks passed
@DougGregor DougGregor deleted the convert-json-to-codable branch May 7, 2024 04:01
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.

None yet

2 participants