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

feat: add support for multi select hash to JMESPath visitor #931

Merged
merged 6 commits into from
Aug 23, 2023

Conversation

0marperez
Copy link
Contributor

Issue #

closes #930

Description of changes

  • Added support for multi select hash
  • Added missing list from multi select list to match JMESPath syntax
  • ensureNullGuard is now addNullGuard and is added to all temporary val's
  • subField function now allows accessing members of shapes that "are not there" (multi select hash creates anonymous objects with arbitrary attributes that don't show up in the model)
  • comparator for anyStringEquals and allStringEquals now casts to a list of strings before comparing (anonymous objects have attributes of generic type)

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@0marperez 0marperez added the no-changelog Indicates that a changelog entry isn't required for a pull request. Use sparingly. label Aug 18, 2023
@0marperez 0marperez requested a review from a team as a code owner August 18, 2023 22:23
Comment on lines 371 to 407
private fun ensureNullGuard(shape: Shape?, expr: String, elvisExpr: String? = null): String =
if (shape?.isNullable == true) {
buildString {
append("?.$expr")
elvisExpr?.let { append(" ?: $it") }
}
} else {
".$expr"
private fun addNullGuard(shape: Shape?, expr: String, elvisExpr: String? = null): String =
buildString {
append("?.$expr")
elvisExpr?.let { append(" ?: $it") }
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: Why are we adding null guards to potentially-non-null expressions? We made the codegen visitor null-aware in response to a waiters bug and I worry that regressing that behavior could introduce new bugs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The complexity didn't seem worth it, now that I know the reason behind it it makes sense. It's also very hard to work with. I'll add it back it's just going to take a little while

Comment on lines 89 to 98
val comparison = when (matcher.comparator!!) {
PathComparator.STRING_EQUALS -> "${actual.identifier} == ${expected.dq()}"
PathComparator.BOOLEAN_EQUALS -> "${actual.identifier} == ${expected.toBoolean()}"
PathComparator.ANY_STRING_EQUALS -> "${actual.identifier}?.any { it == ${expected.dq()} } ?: false"
PathComparator.ANY_STRING_EQUALS -> "(${actual.identifier} as List<String>?)?.any { it == ${expected.dq()} } ?: false"

// NOTE: the isNotEmpty check is necessary because the waiter spec says that `allStringEquals` requires
// at least one value unlike Kotlin's `all` which returns true if the collection is empty
PathComparator.ALL_STRING_EQUALS ->
"!${actual.identifier}.isNullOrEmpty() && ${actual.identifier}.all { it == ${expected.dq()} }"
"!(${actual.identifier} as List<String>).isNullOrEmpty() && ${actual.identifier}.all { it == ${expected.dq()} }"
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: Why is this new cast to List<String> necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Long explanation but when multi select hash makes a selection that selection is stored in an object. Something like this: class Selection<T>(val x: T, val y: T).

The .isNullOrEmpty() function can only be applied to arrays, lists and such.

So at runtime when it was detecting the selection properties as generic (x or y) I was getting errors (because x or y didn't show up as a list, array, etc), that cast fixed them.

Comment on lines 109 to 120
var unwrapExpr: String? = null

if (member != null) {
val memberTarget = ctx.model.expectShape(member.target)
unwrapExpr = when {
memberTarget.isEnum -> "value"
memberTarget.isEnumList -> "map { it.value }"
memberTarget.isEnumMap -> "mapValues { (_, v) -> v.value }"
memberTarget.isBlobShape || memberTarget.isTimestampShape ->
throw CodegenException("acceptor behavior for shape type ${memberTarget.type} is undefined")
else -> null
}
Copy link
Contributor

Choose a reason for hiding this comment

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

To avoid using a var here, you could use a null-check on member like this:

val unwrapExpr = member?.let {
	val memberTarget = ctx.model.expectShape(member.target)
	when {
		memberTarget.isEnum -> "value"
		memberTarget.isEnumList -> "map { it.value }"
		memberTarget.isEnumMap -> "mapValues { (_, v) -> v.value }"
		memberTarget.isBlobShape || memberTarget.isTimestampShape ->
			throw CodegenException("acceptor behavior for shape type ${memberTarget.type} is undefined")
                else -> null
	}
}

}

shapeCursor.addLast(member)
if (member != null) shapeCursor.addLast(member)
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment here about using the Kotlin null helper methods: member?.let { shapeCursor.addLast(it) }

Comment on lines 246 to 255
val attributes = StringBuilder()
var i = 0
expression.expressions.forEach {
if (i == expression.expressions.size - 1) {
attributes.append("val ${it.key}: T")
} else {
attributes.append("val ${it.key}: T, ")
}
i++
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Kotlin has a joinToString function which handles this nicely. The default is a comma-separated string so you don't need to customize the parameters, just the string transformation.

val attributes = expression.expressions.joinToString {
	"val ${it.key}: T" 
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, I wish I knew about this earlier

Comment on lines 262 to 272
val identifiers = StringBuilder()
i = 0
expression.expressions.forEach {
val identifier = addTempVar(it.key, acceptSubexpression(it.value).identifier)
if (i == expression.expressions.size - 1) {
identifiers.append(identifier)
} else {
identifiers.append("$identifier, ")
}
i++
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment about joinToString

Copy link
Contributor

@ianbotsf ianbotsf left a comment

Choose a reason for hiding this comment

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

Minor feedback, fix and ship!

Comment on lines 249 to 253
val properties = mutableListOf<String>()
expression.expressions.forEach {
properties.add("val ${it.key}: T")
}
writer.write("class Selection<T>(${properties.joinToString()})")
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Mutable lists are rarely necessary within a block. This, for instance, can be written as a single joinToString translation:

val properties = expression.expressions.joinToString { "val ${it.key}: T" }
writer.write("class Selection<T>($properties)")

Comment on lines 259 to 264
val identifiers = mutableListOf<String>()
expression.expressions.forEach {
val identifier = addTempVar(it.key, it.value.accept(this).identifier)
identifiers.add(identifier)
}
writer.write("Selection(${identifiers.joinToString()})")
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Also could be simplified to avoid mutability:

val identifiers = expression.expressions.joinToString { addTempVar(it.key, it.value.accept(this).identifier) }
writer.write("Selection($identifiers)")

Comment on lines 256 to 267
writer.openBlock("val #L = listOfNotNull(", listName)
writer.openBlock("run {")

val identifiers = mutableListOf<String>()
expression.expressions.forEach {
val identifier = addTempVar(it.key, it.value.accept(this).identifier)
identifiers.add(identifier)
}
writer.write("Selection(${identifiers.joinToString()})")

writer.closeBlock("},")
writer.closeBlock(")")
Copy link
Contributor

Choose a reason for hiding this comment

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

Style: Favor withBlock over explicit openBlock/closeBlock:

writer.withBlock("val #L = listOfNotNull(", ")", listName) {
    withBlock("run {", "},") {
        // ...generate `Selection` class...
    }
}

@sonarcloud
Copy link

sonarcloud bot commented Aug 22, 2023

SonarCloud Quality Gate failed.    Quality Gate failed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 0 Code Smells

No Coverage information No Coverage information
22.5% 22.5% Duplication

idea Catch issues before they fail your Quality Gate with our IDE extension sonarlint SonarLint

@0marperez 0marperez merged commit ffb5a87 into main Aug 23, 2023
8 of 9 checks passed
@0marperez 0marperez deleted the jmes-path-support-multiselect branch August 23, 2023 15:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
no-changelog Indicates that a changelog entry isn't required for a pull request. Use sparingly.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support for "MultiSelectHash" to JMESPath visitor
3 participants