Skip to content

Commit

Permalink
Merge #7816
Browse files Browse the repository at this point in the history
7816: IDE: convert JSON object to Rust struct after paste r=dima74 a=Kobzol

This PR adds a copy paste processor that tries to convert pasted JSON-like text to a Rust struct (according to the `serde` data model). Currently it does not actually implement any `serde` things on the struct to keep this PR simpler (but it's easy to add `Serialize/Deserialize` derive to the struct using existing intentions and completion).

It currently works like this:
![json](https://user-images.githubusercontent.com/4539057/136369971-6dd99a4d-372d-4ebc-ba78-aed3bbb17850.gif)

Improvements (possibly for follow-up PRs):
- [ ] Add `#[derive(Serialize, Deserialize)]`, possibly with auto import of these traits.
- [ ] Add `serde` with `derive` feature (or `serde_derive` in edition 2015) to `Cargo.toml` if it's not already included (#7405)
- [ ] Parametrize the behaviour in settings (allow to turn it off). Not sure if this is needed `@Undin?`

Fixes: #7783

changelog: You can now paste JSON objects into Rust files and convert them to a Rust struct.

Co-authored-by: Jakub Beránek <berykubik@gmail.com>
  • Loading branch information
bors[bot] and Kobzol committed Feb 21, 2022
2 parents ba7d334 + af274ba commit 6ca1a7a
Show file tree
Hide file tree
Showing 5 changed files with 895 additions and 0 deletions.
168 changes: 168 additions & 0 deletions src/main/kotlin/org/rust/ide/typing/paste/JsonStructParser.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
* Use of this source code is governed by the MIT license that can be
* found in the LICENSE file.
*/

package org.rust.ide.typing.paste

import com.fasterxml.jackson.core.JsonFactoryBuilder
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import com.fasterxml.jackson.core.json.JsonReadFeature
import java.io.IOException

sealed class DataType {
object Integer : DataType()
object Float : DataType()
object Boolean : DataType()
object String : DataType()
object Unknown : DataType()
data class StructRef(val struct: Struct) : DataType()
data class Array(val type: DataType) : DataType()
data class Nullable(val type: DataType) : DataType()
}

// Structs with the same field names and types are considered the same, regardless of field order.
// LinkedHashMap::equals ignores key (field) order.
data class Struct(val fields: LinkedHashMap<String, DataType>)

/**
* Try to parse the input text as a JSON object.
*
* Extract a list of (unique) structs that were encountered in the JSON object.
*/
fun extractStructsFromJson(text: String): List<Struct>? {
// Fast path to avoid creating a parser if the input does not look like a JSON object
val input = text.trim()
if (input.isEmpty() || input.first() != '{' || input.last() != '}') return null

return try {
val factory = JsonFactoryBuilder()
.enable(JsonReadFeature.ALLOW_TRAILING_COMMA)
.build()
val parser = factory.createParser(input)

val structParser = StructParser()
val rootStruct = parser.use {
// There must be an object at the root
val token = parser.nextToken()
if (token != JsonToken.START_OBJECT) return null

structParser.parseStruct(parser)
} ?: return null

// Return structs in reversed order to generate the inner structs first
gatherEncounteredStructs(rootStruct).toList().reversed()
} catch (e: IOException) {
null
}
}

data class Field(val name: String, val type: DataType)

/**
* Finds all unique structs contained in the root struct.
*/
fun gatherEncounteredStructs(root: Struct): Set<Struct> {
val structs = linkedSetOf<Struct>()
gatherStructs(DataType.StructRef(root), structs)
return structs
}

private fun gatherStructs(type: DataType, structs: MutableSet<Struct>) {
when (type) {
is DataType.Array -> gatherStructs(type.type, structs)
is DataType.Nullable -> gatherStructs(type.type, structs)
is DataType.StructRef -> {
structs.add(type.struct)
for (fieldType in type.struct.fields.values) {
gatherStructs(fieldType, structs)
}
}
DataType.Boolean, DataType.Float, DataType.Integer,
DataType.String, DataType.Unknown -> {}
}
}

private class StructParser {
private val structMap = linkedSetOf<Struct>()

fun parseStruct(parser: JsonParser): Struct? {
if (parser.currentToken != JsonToken.START_OBJECT) return null

val fields = linkedMapOf<String, DataType>()
while (true) {
val field = parseField(parser) ?: break

// Ignore duplicate fields
if (field.name in fields) continue
fields[field.name] = field.type
}

if (parser.currentToken != JsonToken.END_OBJECT) return null

val struct = Struct(fields)
if (struct !in structMap) {
structMap.add(struct)
}
return struct
}

private fun parseField(parser: JsonParser): Field? {
if (parser.nextToken() != JsonToken.FIELD_NAME) return null

val name = parser.currentName
val type = parseDataType(parser)
return Field(name, type)
}

private fun parseArray(parser: JsonParser): DataType? {
if (parser.currentToken != JsonToken.START_ARRAY) return null

val foundDataTypes = linkedSetOf<DataType>()
val firstType = parseDataType(parser)

// Empty array
if (firstType == DataType.Unknown) {
return DataType.Array(DataType.Unknown)
}

foundDataTypes.add(firstType)
while (parser.currentToken != null) {
val dataType = parseDataType(parser)
if (dataType == DataType.Unknown && parser.currentToken == JsonToken.END_ARRAY) {
break
}
foundDataTypes.add(dataType)
}

val containsNull = foundDataTypes.any { it is DataType.Nullable }
val innerType = when {
foundDataTypes.size == 1 -> foundDataTypes.first()
foundDataTypes.size == 2 && containsNull -> DataType.Nullable(foundDataTypes.first())
else -> DataType.Unknown
}

return DataType.Array(innerType)
}

private fun parseDataType(parser: JsonParser): DataType {
return when (parser.nextToken()) {
JsonToken.START_OBJECT -> {
val struct = parseStruct(parser)
if (struct == null) {
DataType.Unknown
} else {
DataType.StructRef(struct)
}
}
JsonToken.START_ARRAY -> parseArray(parser) ?: DataType.Unknown
JsonToken.VALUE_NULL -> DataType.Nullable(DataType.Unknown)
JsonToken.VALUE_FALSE, JsonToken.VALUE_TRUE -> DataType.Boolean
JsonToken.VALUE_NUMBER_INT -> DataType.Integer
JsonToken.VALUE_NUMBER_FLOAT -> DataType.Float
JsonToken.VALUE_STRING -> DataType.String
else -> DataType.Unknown
}
}
}
Loading

0 comments on commit 6ca1a7a

Please sign in to comment.