Skip to content

Commit 1a002c6

Browse files
author
yevhenii-nadtochii
committed
Implement standalone DistinctGenerator
1 parent 1094df8 commit 1a002c6

File tree

6 files changed

+170
-44
lines changed

6 files changed

+170
-44
lines changed

java-runtime/src/main/kotlin/io/spine/validate/RuntimeErrorPlaceholder.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ public enum class RuntimeErrorPlaceholder(public val value: String) {
4949
REGEX_PATTERN("regex.pattern"),
5050
REGEX_MODIFIERS("regex.modifiers"),
5151
GOES_COMPANION("goes.companion"),
52-
FIELD_PROPOSED_VALUE("field.proposed_value");
52+
FIELD_PROPOSED_VALUE("field.proposed_value"),
53+
FIELD_DUPLICATES("field.duplicates");
5354

5455
override fun toString(): String = value
5556
}

java/src/main/kotlin/io/spine/validation/java/ClassNames.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,28 @@
2828

2929
package io.spine.validation.java
3030

31+
import com.google.common.collect.HashMultiset
3132
import com.google.common.collect.ImmutableList
33+
import com.google.common.collect.ImmutableSet
34+
import com.google.common.collect.LinkedHashMultiset
35+
import com.google.common.collect.Multiset
3236
import io.spine.base.FieldPath
3337
import io.spine.protodata.java.ClassName
3438
import io.spine.validate.ConstraintViolation
3539
import io.spine.validate.TemplateString
3640
import java.util.regex.Pattern
41+
import java.util.stream.Collectors
3742

3843
/**
3944
* The [ClassName] of [String].
4045
*/
4146
internal val StringClass = ClassName(String::class)
4247

48+
/**
49+
* The [ClassName] of [Collectors].
50+
*/
51+
internal val CollectorsClass = ClassName(Collectors::class)
52+
4353
/**
4454
* The [ClassName] of [TemplateString].
4555
*/
@@ -55,6 +65,21 @@ internal val PatternClass = ClassName(Pattern::class)
5565
*/
5666
internal val ImmutableListClass = ClassName(ImmutableList::class)
5767

68+
/**
69+
* The [ClassName] of [ImmutableSet].
70+
*/
71+
internal val ImmutableSetClass = ClassName(ImmutableSet::class)
72+
73+
/**
74+
* The [ClassName] of [HashMultiset].
75+
*/
76+
internal val LinkedHashMultisetClass = ClassName(LinkedHashMultiset::class)
77+
78+
/**
79+
* The [ClassName] of [Multiset.Entry] class.
80+
*/
81+
internal val MultiSetEntryClass = ClassName(Multiset.Entry::class)
82+
5883
/**
5984
* The [ClassName] of [FieldPath].
6085
*/
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright 2022, TeamDev. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Redistribution and use in source and/or binary forms, with or without
11+
* modification, must retain the above copyright notice and the following
12+
* disclaimer.
13+
*
14+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
16+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
17+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
18+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
20+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25+
*/
26+
27+
package io.spine.validation.java
28+
29+
import io.spine.base.FieldPath
30+
import io.spine.protodata.ast.isList
31+
import io.spine.protodata.ast.isMap
32+
import io.spine.protodata.ast.name
33+
import io.spine.protodata.ast.qualifiedName
34+
import io.spine.protodata.java.CodeBlock
35+
import io.spine.protodata.java.Expression
36+
import io.spine.protodata.java.ReadVar
37+
import io.spine.protodata.java.StringLiteral
38+
import io.spine.protodata.java.call
39+
import io.spine.protodata.java.field
40+
import io.spine.protodata.java.toBuilder
41+
import io.spine.validate.ConstraintViolation
42+
import io.spine.validation.DistinctField
43+
import io.spine.validation.PATTERN
44+
import io.spine.validation.java.ErrorPlaceholder.FIELD_DUPLICATES
45+
import io.spine.validation.java.ErrorPlaceholder.FIELD_PATH
46+
import io.spine.validation.java.ErrorPlaceholder.FIELD_TYPE
47+
import io.spine.validation.java.ErrorPlaceholder.FIELD_VALUE
48+
import io.spine.validation.java.ErrorPlaceholder.PARENT_TYPE
49+
import io.spine.validation.java.ValidationCodeInjector.MessageScope.message
50+
import io.spine.validation.java.ValidationCodeInjector.ValidateScope.parentPath
51+
52+
/**
53+
* The generator for `(distinct)` option.
54+
*
55+
* Generates code for a single field represented by the provided [view].
56+
*/
57+
internal class DistinctFieldGenerator(private val view: DistinctField) {
58+
59+
private val field = view.subject
60+
private val fieldType = field.type
61+
private val getter = message.field(field).getter<List<*>>()
62+
private val declaringType = field.declaringType
63+
64+
fun generate(): FieldOptionCode {
65+
val collection = when {
66+
fieldType.isList -> getter
67+
fieldType.isMap -> getter.call("values")
68+
else -> error("...")
69+
}
70+
val set = ImmutableSetClass.call<Set<*>>("copyOf", collection)
71+
val constraint = CodeBlock(
72+
"""
73+
if ($getter.size() != $set.size()) {
74+
var duplicates = ${calculateDuplicates(collection)};
75+
var fieldPath = ${fieldPath(parentPath)};
76+
var violation = ${violation(ReadVar("fieldPath"), ReadVar("duplicates"))};
77+
violations.add(violation);
78+
}
79+
""".trimIndent()
80+
)
81+
return FieldOptionCode(constraint)
82+
}
83+
84+
// We would like to preserve ordering, so let's use `LinkedHashMultisetClass`.
85+
private fun calculateDuplicates(list: Expression<List<*>>): Expression<List<*>> =
86+
Expression(
87+
"""
88+
$LinkedHashMultisetClass.create($list)
89+
.entrySet()
90+
.stream()
91+
.filter(e -> e.getCount() > 1)
92+
.map($MultiSetEntryClass::getElement)
93+
.collect($CollectorsClass.toList())
94+
""".trimIndent()
95+
)
96+
97+
private fun fieldPath(parent: Expression<FieldPath>): Expression<FieldPath> =
98+
parent.toBuilder()
99+
.chainAdd("field_name", StringLiteral(field.name.value))
100+
.chainBuild()
101+
102+
private fun violation(
103+
fieldPath: Expression<FieldPath>,
104+
duplicates: Expression<List<*>>
105+
): Expression<ConstraintViolation> {
106+
val qualifiedName = field.qualifiedName
107+
val placeholders = supportedPlaceholders(fieldPath, duplicates)
108+
val errorMessage = templateString(view.errorMessage, placeholders, PATTERN, qualifiedName)
109+
return constraintViolation(errorMessage, declaringType, fieldPath, getter)
110+
}
111+
112+
private fun supportedPlaceholders(
113+
fieldPath: Expression<FieldPath>,
114+
duplicates: Expression<List<*>>
115+
): Map<ErrorPlaceholder, Expression<String>> {
116+
val pathAsString = FieldPathsClass.call<String>("getJoined", fieldPath)
117+
return mapOf(
118+
FIELD_PATH to pathAsString,
119+
FIELD_VALUE to fieldType.stringValueOf(getter),
120+
FIELD_TYPE to StringLiteral(fieldType.name),
121+
PARENT_TYPE to StringLiteral(declaringType.qualifiedName),
122+
FIELD_DUPLICATES to duplicates.call("toString")
123+
)
124+
}
125+
}

java/src/main/kotlin/io/spine/validation/java/DistinctGenerator.kt

Lines changed: 15 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -26,53 +26,26 @@
2626

2727
package io.spine.validation.java
2828

29-
import com.google.common.collect.ImmutableSet
30-
import io.spine.protodata.ast.isMap
31-
import io.spine.protodata.java.ClassName
32-
import io.spine.protodata.java.Expression
33-
import io.spine.protodata.java.MethodCall
34-
import io.spine.protodata.java.call
29+
import io.spine.protodata.ast.TypeName
30+
import io.spine.server.query.Querying
31+
import io.spine.server.query.select
32+
import io.spine.validation.DistinctField
3533

3634
/**
37-
* Generates the code for the [DistinctCollection][io.spine.validation.DistinctCollection] operator.
38-
*
39-
* If the generator serves a map, it checks the [values][Map.values] of the map to be distinct.
35+
* The generator for `(distinct)` option.
4036
*/
41-
internal class DistinctGenerator(ctx: GenerationContext) : SimpleRuleGenerator(ctx) {
37+
internal class DistinctGenerator(
38+
private val querying: Querying,
39+
) : OptionGenerator {
4240

43-
/**
44-
* Creates an expression checking that a repeated field or a map of a proto message
45-
* has distinct values.
46-
*
47-
* If the field is a map, the generated code checks the values of the map to be distinct.
48-
*
49-
* The generated code assumes that if the field contains distinct values, the size of
50-
* the original collection is equal to the size of the `ImmutableSet` created as a copy
51-
* of the checked collection.
52-
*/
53-
override fun condition(): Expression<Boolean> {
54-
val values = fieldValues()
55-
return equalityOf(
56-
MethodCall(values, "size"),
57-
ClassName(ImmutableSet::class)
58-
.call<ImmutableSet<*>>("copyOf", values)
59-
.chain("size")
60-
)
41+
private val allDistinctFields by lazy {
42+
querying.select<DistinctField>()
43+
.all()
6144
}
6245

63-
private fun fieldValues(): Expression<Collection<*>> {
64-
val fieldIsMap = ctx.simpleRuleField.isMap
65-
val fieldValue = ctx.fieldOrElement!!
66-
return if (fieldIsMap) {
67-
MethodCall(fieldValue, "values")
68-
} else {
69-
// `DistinctGenerator` can be instantiated only for repeated fields and maps in
70-
// `io.spine.validation.java.generatorForCustom`. So, the cast is safe.
71-
@Suppress("UNCHECKED_CAST")
72-
fieldValue as Expression<Collection<*>>
73-
}
46+
override fun codeFor(type: TypeName): List<FieldOptionCode> {
47+
val distinctFields = allDistinctFields.filter { it.id.type == type }
48+
val generatedFields = distinctFields.map { DistinctFieldGenerator(it).generate() }
49+
return generatedFields
7450
}
75-
76-
private fun equalityOf(left: Expression<Int>, right: Expression<Int>): Expression<Boolean> =
77-
Expression(left.toCode() + " == " + right.toCode())
7851
}

java/src/main/kotlin/io/spine/validation/java/ErrorPlaceholder.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ public enum class ErrorPlaceholder(public val value: String) {
5656
REGEX_PATTERN("regex.pattern"),
5757
REGEX_MODIFIERS("regex.modifiers"),
5858
GOES_COMPANION("goes.companion"),
59-
FIELD_PROPOSED_VALUE("field.proposed_value");
59+
FIELD_PROPOSED_VALUE("field.proposed_value"),
60+
FIELD_DUPLICATES("field.duplicates");
6061

6162
override fun toString(): String = value
6263
}

java/src/main/kotlin/io/spine/validation/java/JavaValidationRenderer.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public class JavaValidationRenderer : JavaRenderer() {
5454
RequiredGenerator(querying = this, valueConverter),
5555
PatternGenerator(querying = this),
5656
GoesGenerator(querying = this, valueConverter),
57+
DistinctGenerator(querying = this),
5758
)
5859
}
5960

0 commit comments

Comments
 (0)