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

Implement $addFields stage dsl #103

Merged
merged 9 commits into from
Oct 29, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.github.inflab.spring.data.mongodb.core.aggregation

import com.github.inflab.spring.data.mongodb.core.aggregation.expression.AggregationExpressionDsl
import com.github.inflab.spring.data.mongodb.core.annotation.AggregationMarker
import com.github.inflab.spring.data.mongodb.core.extension.toDotPath
import org.springframework.data.mongodb.core.aggregation.AddFieldsOperation
import org.springframework.data.mongodb.core.aggregation.AggregationExpression
import kotlin.reflect.KProperty

/**
* A Kotlin DSL to configure $addFields stage using idiomatic Kotlin code.
*
* @author username1103
* @since 1.0
* @see <a href="https://www.mongodb.com/docs/v7.0/reference/operator/aggregation/addFields">$addFields (aggregation)</a>
*/
@AggregationMarker
class AddFieldsStageDsl {
private val builder = AddFieldsOperation.builder()

/**
* Adds new fields to documents with aggregation expression.
*
* @param configuration The configuration block where you can use DSL to define aggregation expression.
*/
infix fun String.set(configuration: AggregationExpressionDsl.() -> AggregationExpression) {
builder.addField(this).withValue(AggregationExpressionDsl().configuration())
}

/**
* Adds new fields to documents from a field.
*
* @param path The path of the field to contain value to be added.
*/
infix fun String.set(path: KProperty<Any?>) {
builder.addField(this).withValue("${'$'}${path.toDotPath()}")
}

/**
* Adds new fields to documents with a value.
*
* @param value The value of the field to add.
*/
infix fun String.set(value: Any?) {
builder.addField(this).withValue(value)
}

/**
* Adds new fields to documents from a field.
*
* @param fieldPath The path of the field to contain value to be added.
*/
infix fun String.setByField(fieldPath: String) {
builder.addField(this).withValue("$$fieldPath")
Copy link
Contributor

Choose a reason for hiding this comment

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

There is withValueOf method for field path.
But test case is failed if used it.
Bad luck 😢

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. So use withValue 🥲

}

/**
* Adds new fields to documents with aggregation expression.
*
* @param configuration The configuration block where you can use DSL to define aggregation expression.
*/
infix fun KProperty<Any?>.set(configuration: AggregationExpressionDsl.() -> AggregationExpression) {
builder.addField(this.toDotPath()).withValue(AggregationExpressionDsl().configuration())
}

/**
* Adds new fields to documents from a field.
*
* @param path The path of the field to contain value to be added.
*/
infix fun KProperty<Any?>.set(path: KProperty<Any?>) {
builder.addField(this.toDotPath()).withValue("${'$'}${path.toDotPath()}")
}

/**
* Adds new fields to documents with a value.
*
* @param value The value of the field to add.
*/
infix fun KProperty<Any?>.set(value: Any?) {
builder.addField(this.toDotPath()).withValue(value)
}

/**
* Adds new fields to documents from a field.
*
* @param fieldPath The path of the field to contain value to be added.
*/
infix fun KProperty<Any?>.setByField(fieldPath: String) {
builder.addField(this.toDotPath()).withValue("$$fieldPath")
}

internal fun get() = builder.build()
}
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,16 @@ class AggregationDsl {
operations += GroupStageDsl().apply(configuration).build()
}

/**
* Configures a stage that add fields in documents.
*
* @param configuration The configuration block for the [AddFieldsStageDsl].
* @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/addFields">$addFields (aggregation)</a>
*/
fun addFields(configuration: AddFieldsStageDsl.() -> Unit) {
operations += AddFieldsStageDsl().apply(configuration).get()
}

/**
* Builds the [Aggregation] using the configured [AggregationOperation]s.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.github.inflab.spring.data.mongodb.core.mapping

import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.mapping.Field
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
Expand Down Expand Up @@ -30,16 +31,36 @@ internal fun asString(property: KProperty<*>): String =
}

/**
* Get field name from [Field] annotation or property name.
* Get field name from [Field] annotation, [Id] annotation or property name.
*
* @param property property to get field name from
* @author Jake Son
* @since 1.0
*/
@Suppress("detekt:style:ReturnCount")
internal fun toFieldName(property: KProperty<*>): String {
val fieldAnnotation = property.javaField?.getAnnotation(Field::class.java) ?: return property.name
val idAnnotation = property.javaField?.getAnnotation(Id::class.java)
if (idAnnotation != null) {
return "_id"
}

val fieldAnnotation = property.javaField?.getAnnotation(Field::class.java)

if (fieldAnnotation != null) {
if (fieldAnnotation.value.isNotEmpty()) {
return fieldAnnotation.value
}

if (fieldAnnotation.name.isNotEmpty()) {
return fieldAnnotation.name
}
}

if (property.name == "id") {
return "_id"
}

return fieldAnnotation.value.ifEmpty { fieldAnnotation.name.ifEmpty { property.name } }
return property.name
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package com.github.inflab.spring.data.mongodb.core.aggregation

import com.github.inflab.spring.data.mongodb.core.util.shouldBeJson
import io.kotest.core.spec.style.FreeSpec

internal class AddFieldsStageDslTest : FreeSpec({

fun addFields(configuration: AddFieldsStageDsl.() -> Unit): AddFieldsStageDsl =
AddFieldsStageDsl().apply(configuration)

"set" - {
"should set field with path" {
// given
data class Test(val targetPath: Int)
val stage = addFields {
"a" set Test::targetPath
}

// when
val result = stage.get()

// then
result.shouldBeJson(
"""
{
"${'$'}addFields": {
"a": "${'$'}targetPath"
}
}
""".trimIndent(),
)
}

"should set field path with path" {
// given
data class Test(val sourcePath: Int, val targetPath: Int)
val stage = addFields {
Test::targetPath set Test::sourcePath
}

// when
val result = stage.get()

// then
result.shouldBeJson(
"""
{
"${'$'}addFields": {
"targetPath": "${'$'}sourcePath"
}
}
""".trimIndent(),
)
}

"should set field with aggregation expression" {
// given
data class Test(val targetPath: Int)
val stage = addFields {
"a" set {
add { of(1) and 2 and Test::targetPath }
}
}

// when
val result = stage.get()

// then
result.shouldBeJson(
"""
{
"${'$'}addFields": {
"a": {
"${'$'}add": [
1,
2,
"${'$'}targetPath"
]
}
}
}
""".trimIndent(),
)
}

"should set field path with aggregation expression" {
// given
data class Test(val sourcePath: Int, val targetPath: Int)
val stage = addFields {
Test::targetPath set {
add { of(1) and 2 and Test::sourcePath }
}
}

// when
val result = stage.get()

// then
result.shouldBeJson(
"""
{
"${'$'}addFields": {
"targetPath": {
"${'$'}add": [
1,
2,
"${'$'}sourcePath"
]
}
}
}
""".trimIndent(),
)
}

"should set field with value" {
// given
val stage = addFields {
"a" set 1
}

// when
val result = stage.get()

// then
result.shouldBeJson(
"""
{
"${'$'}addFields": {
"a": 1
}
}
""".trimIndent(),
)
}

"should set field path with value" {
// given
data class Test(val targetPath: Int)
val stage = addFields {
Test::targetPath set 1
}

// when
val result = stage.get()

// then
result.shouldBeJson(
"""
{
"${'$'}addFields": {
"targetPath": 1
}
}
""".trimIndent(),
)
}
}

"setByField" - {
"should set field with value of other field" {
// given
val stage = addFields {
"a" setByField "b"
}

// when
val result = stage.get()

// then
result.shouldBeJson(
"""
{
"${'$'}addFields": {
"a": "${'$'}b"
}
}
""".trimIndent(),
)
}

"should set field path with value of other field" {
// given
data class Test(val targetPath: Int)
val stage = addFields {
Test::targetPath setByField "b"
}

// when
val result = stage.get()

// then
result.shouldBeJson(
"""
{
"${'$'}addFields": {
"targetPath": "${'$'}b"
}
}
""".trimIndent(),
)
}
}
})
Loading