Skip to content

Commit

Permalink
resolves #99 easier projection for one, two or three fields
Browse files Browse the repository at this point in the history
  • Loading branch information
zigzago committed Dec 12, 2018
1 parent 0abaf32 commit ef27151
Show file tree
Hide file tree
Showing 3 changed files with 342 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
package org.litote.kmongo

import org.bson.Document
import org.litote.kmongo.model.Coordinate
import org.litote.kmongo.model.Friend
import org.litote.kmongo.util.SingleProjection
import kotlin.test.Test
import kotlin.test.assertEquals

Expand All @@ -31,6 +33,8 @@ class ProjectionTest : AllCategoriesKMongoBaseTest<Friend>() {
//Document extension methods
//singleProjection

data class FriendWithNameOnly(val name: String)

@Test
fun `projection works as expected`() {
col.bulkWrite(
Expand All @@ -50,4 +54,85 @@ class ProjectionTest : AllCategoriesKMongoBaseTest<Friend>() {
result
)
}

@Test
fun `projection without mandatory field is ok if not retrieved in projection`() {
col.bulkWrite(
insertOne(Friend("Joe")),
insertOne(Friend("Bob"))
)
val result: Iterable<String> =
col.withDocumentClass<FriendWithNameOnly>()
.find()
.descendingSort(FriendWithNameOnly::name)
.projection(FriendWithNameOnly::name)
.map { it.name }
.toList()

assertEquals(
listOf("Joe", "Bob"),
result
)
}

@Test
fun `single projection is ok`() {
col.bulkWrite(
insertOne(Friend("Joe")),
insertOne(Friend("Bob"))
)
val result: List<String?> =
col.projection(Friend::name, Friend::name eq "Joe")
.toList()

assertEquals(
listOf("Joe"),
result
)

val result2: List<String?> =
col.projection(Friend::name, options = { it.descendingSort(SingleProjection<*>::field) })
.toList()

assertEquals(
listOf("Joe", "Bob"),
result2
)

}

@Test
fun `pair projection is ok`() {
col.bulkWrite(
insertOne(Friend("Joe", coordinate = Coordinate(1, 2))),
insertOne(Friend("Bob", coordinate = Coordinate(3, 4)))
)
val result: Pair<String?, Coordinate?>? =
col.projection(Friend::name, Friend::coordinate, Friend::name eq "Joe").first()

assertEquals(
"Joe" to Coordinate(1, 2),
result
)
}

@Test
fun `triple projection is ok`() {
col.bulkWrite(
insertOne(Friend("Joe", "Here", coordinate = Coordinate(1, 2), tags = listOf("t1"))),
insertOne(Friend("Bob", "Here", coordinate = Coordinate(3, 4), tags = listOf("t2")))
)
val result: Triple<String?, Coordinate?, List<String>?>? =
col.projection(
Friend::name,
Friend::coordinate,
Friend::tags,
Friend::name eq "Joe"
).first()

assertEquals(
Triple("Joe", Coordinate(1, 2), listOf("t1")),
result
)
}
}
79 changes: 79 additions & 0 deletions kmongo-core/src/main/kotlin/org/litote/kmongo/extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ import org.litote.kmongo.util.KMongoUtil.toBson
import org.litote.kmongo.util.KMongoUtil.toBsonList
import org.litote.kmongo.util.KMongoUtil.toBsonModifier
import org.litote.kmongo.util.KMongoUtil.toWriteModel
import org.litote.kmongo.util.PairProjection
import org.litote.kmongo.util.SingleProjection
import org.litote.kmongo.util.TripleProjection
import org.litote.kmongo.util.pairProjectionCodecRegistry
import org.litote.kmongo.util.singleProjectionCodecRegistry
import org.litote.kmongo.util.tripleProjectionCodecRegistry
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
Expand Down Expand Up @@ -882,6 +888,79 @@ fun <T> FindIterable<T>.projection(vararg projections: KProperty<*>): FindIterab
projection(include(*projections))


/**
* Returns the specified field for all matching documents.
*
* @param property the property to return
* @param query the optional find query
* @param options the optional [FindIterable] modifiers
* @return a property value iterable
*/
inline fun <T, reified F> MongoCollection<T>.projection(
property: KProperty<F>,
query: Bson = EMPTY_BSON,
options: (FindIterable<SingleProjection<F>>) -> FindIterable<SingleProjection<F>> = { it }
): MongoIterable<F> =
withDocumentClass<SingleProjection<F>>()
.withCodecRegistry(singleProjectionCodecRegistry<F>(codecRegistry))
.find(query)
.let { options(it) }
.projection(fields(excludeId(), include(property)))
.map { it.field }

/**
* Returns the specified two fields for all matching documents.
*
* @param property1 the first property to return
* @param property2 the second property to return
* @param query the optional find query
* @param options the optional [FindIterable] modifiers
* @return a pair of property values iterable
*/
inline fun <T, reified F1, reified F2> MongoCollection<T>.projection(
property1: KProperty<F1>,
property2: KProperty<F2>,
query: Bson = EMPTY_BSON,
options: (FindIterable<PairProjection<F1, F2>>) -> FindIterable<PairProjection<F1, F2>> = { it }
): MongoIterable<Pair<F1?, F2?>> =
withDocumentClass<PairProjection<F1, F2>>()
.withCodecRegistry(pairProjectionCodecRegistry<F1, F2>(property1.path(), property2.path(), codecRegistry))
.find(query)
.let { options(it) }
.projection(fields(excludeId(), include(property1), include(property2)))
.map { it.field1 to it.field2 }

/**
* Returns the specified three fields for all matching documents.
*
* @param property1 the first property to return
* @param property2 the second property to return
* @param property3 the third property to return
* @param query the optional find query
* @param options the optional [FindIterable] modifiers
* @return a triple of property values iterable
*/
inline fun <T, reified F1, reified F2, reified F3> MongoCollection<T>.projection(
property1: KProperty<F1>,
property2: KProperty<F2>,
property3: KProperty<F3>,
query: Bson = EMPTY_BSON,
options: (FindIterable<TripleProjection<F1, F2, F3>>) -> FindIterable<TripleProjection<F1, F2, F3>> = { it }
): MongoIterable<Triple<F1?, F2?, F3?>> =
withDocumentClass<TripleProjection<F1, F2, F3>>()
.withCodecRegistry(
tripleProjectionCodecRegistry<F1, F2, F3>(
property1.path(),
property2.path(),
property3.path(),
codecRegistry
)
)
.find(query)
.let { options(it) }
.projection(fields(excludeId(), include(property1), include(property2), include(property3)))
.map { Triple(it.field1, it.field2, it.field3) }

/**
* Sets the sort criteria to apply to the query.
*
Expand Down
178 changes: 178 additions & 0 deletions kmongo-shared/src/main/kotlin/org/litote/kmongo/util/Projections.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/*
* Copyright (C) 2017/2018 Litote
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.litote.kmongo.util

import org.bson.BsonReader
import org.bson.BsonWriter
import org.bson.codecs.Codec
import org.bson.codecs.DecoderContext
import org.bson.codecs.EncoderContext
import org.bson.codecs.configuration.CodecRegistries
import org.bson.codecs.configuration.CodecRegistry

/**
* Single projection (one field only).
*/
class SingleProjection<F>(val field: F?)

/**
* Pair projection (two fields).
*/
class PairProjection<F1, F2>(val field1: F1?, val field2: F2?)

/**
* Triple projection (three fields).
*/
class TripleProjection<F1, F2, F3>(val field1: F1?, val field2: F2?, val field3: F3?)

/**
* This method is not part of the public API and may be removed or changed at any time.
*/
inline fun <reified T> singleProjectionCodecRegistry(
baseRegistry: CodecRegistry
): CodecRegistry {
return CodecRegistries.fromRegistries(
CodecRegistries.fromCodecs(
object : Codec<SingleProjection<*>> {
override fun getEncoderClass(): Class<SingleProjection<*>> = SingleProjection::class.java

override fun encode(
writer: BsonWriter,
value: SingleProjection<*>,
encoderContext: EncoderContext
) {
error("not supported")
}

override fun decode(reader: BsonReader, decoderContext: DecoderContext): SingleProjection<*> {
reader.readStartDocument()
reader.readName()
val codec = baseRegistry.get(T::class.java)
val r = codec.decode(reader, decoderContext)
reader.readEndDocument()
return SingleProjection(r)
}
}
),
baseRegistry
)
}

/**
* This method is not part of the public API and may be removed or changed at any time.
*/
inline fun <reified T1, reified T2> pairProjectionCodecRegistry(
property1: String,
property2: String,
baseRegistry: CodecRegistry
): CodecRegistry {
return CodecRegistries.fromRegistries(
CodecRegistries.fromCodecs(
object : Codec<PairProjection<*, *>> {
override fun getEncoderClass(): Class<PairProjection<*, *>> = PairProjection::class.java

override fun encode(
writer: BsonWriter,
value: PairProjection<*, *>,
encoderContext: EncoderContext
) {
error("not supported")
}

override fun decode(reader: BsonReader, decoderContext: DecoderContext): PairProjection<*, *> {
reader.readStartDocument()
var r1: T1? = null
var r2: T2? = null
try {
while (r1 == null || r2 == null)
when (reader.readName()) {
property1 -> {
val codec1 = baseRegistry.get(T1::class.java)
r1 = codec1.decode(reader, decoderContext)
}
property2 -> {
val codec2 = baseRegistry.get(T2::class.java)
r2 = codec2.decode(reader, decoderContext)
}
}
} catch (e: Exception) {
//ignore
}
reader.readEndDocument()
return PairProjection(r1, r2)
}
}
),
baseRegistry
)
}

/**
* This method is not part of the public API and may be removed or changed at any time.
*/
inline fun <reified T1, reified T2, reified T3> tripleProjectionCodecRegistry(
property1: String,
property2: String,
property3: String,
baseRegistry: CodecRegistry
): CodecRegistry {

return CodecRegistries.fromRegistries(
CodecRegistries.fromCodecs(
object : Codec<TripleProjection<*, *, *>> {
override fun getEncoderClass(): Class<TripleProjection<*, *, *>> = TripleProjection::class.java

override fun encode(
writer: BsonWriter,
value: TripleProjection<*, *, *>,
encoderContext: EncoderContext
) {
error("not supported")
}

override fun decode(reader: BsonReader, decoderContext: DecoderContext): TripleProjection<*, *, *> {
reader.readStartDocument()
var r1: T1? = null
var r2: T2? = null
var r3: T3? = null
try {
while (r1 == null || r2 == null || r3 == null)
when (reader.readName()) {
property1 -> {
val codec1 = baseRegistry.get(T1::class.java)
r1 = codec1.decode(reader, decoderContext)
}
property2 -> {
val codec2 = baseRegistry.get(T2::class.java)
r2 = codec2.decode(reader, decoderContext)
}
property3 -> {
val codec3 = baseRegistry.get(T3::class.java)
r3 = codec3.decode(reader, decoderContext)
}
}
} catch (e: Exception) {
//ignore
}
reader.readEndDocument()
return TripleProjection(r1, r2, r3)
}
}
),
baseRegistry
)
}

0 comments on commit ef27151

Please sign in to comment.