4 - Add tools to map update request to an update object of the persistent store layer [DPP-1212] (#14858)

changelog_begin
changelog_end
This commit is contained in:
pbatko-da 2022-09-05 14:55:44 +02:00 committed by GitHub
parent 1effd3eab2
commit 0853fc44e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1498 additions and 3 deletions

View File

@ -0,0 +1,26 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.apiserver.update
// TODO um-for-hub: Get field names from files generated from proto files instead of hardcoding them
object FieldNames {
object UpdateUserRequest {
val user = "user"
}
object User {
val primaryParty = "primary_party"
val isDeactivated = "is_deactivated"
val metadata = "metadata"
}
object Metadata {
val annotations = "annotations"
}
object UpdatePartyDetailsRequest {
val partyDetails = "party_details"
}
object PartyDetails {
val localMetadata = "local_metadata"
}
}

View File

@ -0,0 +1,53 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.apiserver.update
import com.daml.ledger.api.domain
import com.daml.ledger.api.domain.ParticipantParty
import com.daml.ledger.participant.state.index.v2.{
AnnotationsUpdate,
ObjectMetaUpdate,
PartyRecordUpdate,
}
object PartyRecordUpdateMapper extends UpdateMapperBase {
import UpdateRequestsPaths.PartyDetailsPaths
type DomainObject = domain.ParticipantParty.PartyRecord
type UpdateObject = PartyRecordUpdate
override val fullUpdateTrie: UpdatePathsTrie = PartyDetailsPaths.fullUpdateTrie
override def makeUpdateObject(
partyRecord: ParticipantParty.PartyRecord,
updateTrie: UpdatePathsTrie,
): Result[PartyRecordUpdate] = {
for {
annotationsUpdate <- resolveAnnotationsUpdate(updateTrie, partyRecord.metadata.annotations)
} yield {
PartyRecordUpdate(
party = partyRecord.party,
metadataUpdate = ObjectMetaUpdate(
resourceVersionO = partyRecord.metadata.resourceVersionO,
annotationsUpdateO = annotationsUpdate,
),
)
}
}
def resolveAnnotationsUpdate(
updateTrie: UpdatePathsTrie,
newValue: Map[String, String],
): Result[Option[AnnotationsUpdate]] =
updateTrie
.findMatch(PartyDetailsPaths.annotations)
.fold(noUpdate[AnnotationsUpdate])(updateMatch =>
makeAnnotationsUpdate(
newValue = newValue,
updateMatch = updateMatch,
)
)
}

View File

@ -0,0 +1,127 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.apiserver.update
import com.daml.ledger.participant.state.index.v2.AnnotationsUpdate
import com.daml.platform.apiserver.update.UpdatePathsTrie.MatchResult
import com.google.protobuf.field_mask.FieldMask
trait UpdateMapperBase {
type DomainObject
type UpdateObject
/** A trie containing all update paths. Used for validating the input update mask paths.
*/
def fullUpdateTrie: UpdatePathsTrie
protected[update] def makeUpdateObject(
domainObject: DomainObject,
updateTrie: UpdatePathsTrie,
): Result[UpdateObject]
/** Validates its input and produces an update object.
* NOTE: The return update object might represent an empty (no-op) update.
*
* @param domainObject represents the new values for the update
* @param updateMask indicates which fields should get updated
*/
final def toUpdate(
domainObject: DomainObject,
updateMask: FieldMask,
): Result[UpdateObject] = {
for {
updateTrie <- makeUpdateTrie(updateMask)
updateObject <- makeUpdateObject(domainObject, updateTrie)
} yield {
updateObject
}
}
private def makeUpdateTrie(updateMask: FieldMask): Result[UpdatePathsTrie] = {
for {
_ <- if (updateMask.paths.isEmpty) Left(UpdatePathError.EmptyFieldMask) else Right(())
parsedPaths <- UpdatePath.parseAll(updateMask.paths)
_ <- validatePathsMatchValidFields(parsedPaths)
updateTrie <- UpdatePathsTrie.fromPaths(parsedPaths)
} yield updateTrie
}
protected[update] final def noUpdate[A]: Result[Option[A]] = Right(None)
protected[update] final def validatePathsMatchValidFields(
paths: Seq[UpdatePath]
): Result[Unit] =
paths.foldLeft[Result[Unit]](Right(())) { (ax, parsedPath) =>
for {
_ <- ax
_ <-
if (!fullUpdateTrie.containsPrefix(parsedPath.fieldPath)) {
Left(UpdatePathError.UnknownFieldPath(parsedPath.toRawString))
} else Right(())
} yield ()
}
protected[update] final def makeAnnotationsUpdate(
newValue: Map[String, String],
updateMatch: MatchResult,
): Result[Option[AnnotationsUpdate]] = {
val isDefaultValue = newValue.isEmpty
val modifier = updateMatch.getUpdatePathModifier
def mergeUpdate = Right(Some(AnnotationsUpdate.Merge.fromNonEmpty(newValue)))
val replaceUpdate = Right(Some(AnnotationsUpdate.Replace(newValue)))
if (updateMatch.isExact) {
modifier match {
case UpdatePathModifier.NoModifier =>
if (isDefaultValue) replaceUpdate else mergeUpdate
case UpdatePathModifier.Merge =>
if (isDefaultValue)
Left(
UpdatePathError.MergeUpdateModifierOnEmptyMapField(
updateMatch.matchedPath.toRawString
)
)
else mergeUpdate
case UpdatePathModifier.Replace =>
replaceUpdate
}
} else {
modifier match {
case UpdatePathModifier.NoModifier | UpdatePathModifier.Merge =>
if (isDefaultValue) noUpdate else mergeUpdate
case UpdatePathModifier.Replace =>
replaceUpdate
}
}
}
protected[update] final def makePrimitiveFieldUpdate[A](
updateMatch: MatchResult,
defaultValue: A,
newValue: A,
): Result[Option[A]] = {
val isDefaultValue = newValue == defaultValue
val modifier = updateMatch.getUpdatePathModifier
val some = Right(Some(newValue))
if (updateMatch.isExact) {
modifier match {
case UpdatePathModifier.NoModifier | UpdatePathModifier.Replace => some
case UpdatePathModifier.Merge =>
if (isDefaultValue)
Left(
UpdatePathError.MergeUpdateModifierOnPrimitiveField(
updateMatch.matchedPath.toRawString
)
)
else some
}
} else {
modifier match {
case UpdatePathModifier.NoModifier | UpdatePathModifier.Merge =>
if (isDefaultValue) noUpdate else some
case UpdatePathModifier.Replace => some
}
}
}
}

View File

@ -0,0 +1,60 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.apiserver.update
case class UpdatePath(fieldPath: List[String], modifier: UpdatePathModifier) {
def toRawString: String = fieldPath.mkString(".") + modifier.toRawString
}
object UpdatePath {
def parseAll(rawPaths: Seq[String]): Result[Seq[UpdatePath]] = {
val parsedPathsResult: Result[Seq[UpdatePath]] = rawPaths
.map { rawPath: String =>
UpdatePath.parseSingle(rawPath)
}
.foldLeft[Result[Seq[UpdatePath]]](Right(Seq.empty)) { (ax, next) =>
for {
a <- ax
b <- next
} yield {
a :+ b
}
}
parsedPathsResult
}
private[update] def parseSingle(rawPath: String): Result[UpdatePath] = {
// TODO um-for-hub major: Document that "!" is a special character in update paths
for {
pathAndModifierO <- {
rawPath.split('!') match {
case Array() => Left(UpdatePathError.EmptyFieldPath(rawPath))
case Array(path) => Right((path, None))
case Array(path, updateModifier) => Right((path, Some(updateModifier)))
case _ => Left(UpdatePathError.InvalidUpdatePathSyntax(rawPath))
}
}
(fieldPathRaw, modifierO) = pathAndModifierO
_ <-
if (fieldPathRaw.isEmpty) {
Left(UpdatePathError.EmptyFieldPath(rawPath))
} else Right(())
modifier <- {
modifierO match {
case None => Right(UpdatePathModifier.NoModifier)
case Some("merge") => Right(UpdatePathModifier.Merge)
case Some("replace") => Right(UpdatePathModifier.Replace)
case _ => Left(UpdatePathError.UnknownUpdateModifier(rawPath))
}
}
} yield {
UpdatePath(
fieldPathRaw.split('.').toList,
modifier,
)
}
}
}

View File

@ -0,0 +1,25 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.apiserver.update
sealed trait UpdatePathError
object UpdatePathError {
final case class MergeUpdateModifierOnEmptyMapField(rawPath: String) extends UpdatePathError
final case class MergeUpdateModifierOnPrimitiveField(rawPath: String) extends UpdatePathError
final case class UnknownFieldPath(rawPath: String) extends UpdatePathError
final case class UnknownUpdateModifier(rawPath: String) extends UpdatePathError
final case class InvalidUpdatePathSyntax(rawPath: String) extends UpdatePathError
final case class EmptyFieldPath(rawPath: String) extends UpdatePathError
final case class DuplicatedFieldPath(rawPath: String) extends UpdatePathError
final case object EmptyFieldMask extends UpdatePathError
}

View File

@ -0,0 +1,20 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.apiserver.update
object UpdatePathModifier {
object Merge extends UpdatePathModifier {
override def toRawString: String = "!merge"
}
object Replace extends UpdatePathModifier {
override def toRawString: String = "!replace"
}
object NoModifier extends UpdatePathModifier {
override def toRawString: String = ""
}
}
sealed trait UpdatePathModifier {
def toRawString: String
}

View File

@ -0,0 +1,156 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.apiserver.update
import collection.mutable
import scala.annotation.tailrec
import scala.collection.immutable.SortedMap
object UpdatePathsTrie {
case class MatchResult(
isExact: Boolean,
matchedPath: UpdatePath,
) {
def getUpdatePathModifier: UpdatePathModifier = matchedPath.modifier
}
def apply(
modifierO: Option[UpdatePathModifier],
nodes: (String, UpdatePathsTrie)*
) = new UpdatePathsTrie(
nodes = SortedMap.from(nodes),
modifierO = modifierO,
)
def fromPaths(
paths: Seq[List[String]]
)(implicit dummy: DummyImplicit): Result[UpdatePathsTrie] = {
fromPaths(paths.map(UpdatePath(_, modifier = UpdatePathModifier.NoModifier)))
}
def fromPaths(paths: Seq[UpdatePath]): Result[UpdatePathsTrie] = {
val builder: Result[Builder] = Right(Builder(None))
val buildResult = paths.foldLeft(builder)((builderResult, path) =>
for {
b <- builderResult
_ <- b.insertUniquePath(path)
} yield b
)
buildResult.map(build)
}
private def build(builder: Builder): UpdatePathsTrie = {
new UpdatePathsTrie(
modifierO = builder.modifierO,
nodes = SortedMap.from(builder.nodes.view.mapValues(build)),
)
}
private object Builder {
def apply(
modifierO: Option[UpdatePathModifier],
nodes: (String, Builder)*
) = new Builder(
nodes = mutable.SortedMap.from(nodes),
modifierO = modifierO,
)
}
private case class Builder(
nodes: mutable.SortedMap[String, Builder],
var modifierO: Option[UpdatePathModifier],
) {
/** @param updatePath unique path to be inserted
*/
def insertUniquePath(updatePath: UpdatePath): Result[Unit] = {
if (!doInsertUniquePath(updatePath.fieldPath, updatePath.modifier)) {
Left(UpdatePathError.DuplicatedFieldPath(updatePath.toRawString))
} else Right(())
}
/** @return true if successfully inserted the field path, false if the field path was already present in this trie
*/
@tailrec
private def doInsertUniquePath(
fieldPath: List[String],
modifier: UpdatePathModifier,
): Boolean = {
fieldPath match {
case Nil =>
if (this.modifierO.nonEmpty) {
false
} else {
this.modifierO = Some(modifier)
true
}
case key :: subpath =>
if (!nodes.contains(key)) {
val empty = new Builder(nodes = mutable.SortedMap.empty, modifierO = None)
nodes.put(key, empty)
}
nodes(key).doInsertUniquePath(subpath, modifier)
}
}
}
}
/** Data structure for storing and querying update paths.
*
* Each update path specifies:
* - a field path corresponding to a field of an update request proto message,
* - an update modifier.
*
* See also [[com.google.protobuf.field_mask.FieldMask]]).
*
* @param modifierO - non empty to signify a path ending in this node exists
*/
private[update] case class UpdatePathsTrie(
nodes: SortedMap[String, UpdatePathsTrie],
modifierO: Option[UpdatePathModifier],
) {
import UpdatePathsTrie._
/** @return true if 'path' matches some prefix of some field path
*/
@tailrec
final def containsPrefix(path: List[String]): Boolean = {
path match {
case Nil => true
case head :: rest if nodes.contains(head) => nodes(head).containsPrefix(rest)
case _ => false
}
}
/** There is a match if this trie contains 'path' or if it contains a prefix of 'path'.
* @return the match corresponding to the longest matched field path, none otherwise.
*/
def findMatch(path: List[String]): Option[MatchResult] = {
findPath(path) match {
case Some(modifier) =>
Some(MatchResult(isExact = true, matchedPath = UpdatePath(path, modifier)))
case None =>
val properPrefixesLongestFirst =
path.inits.filter(init => init.size != path.size).toList.sortBy(-_.length)
properPrefixesLongestFirst.iterator
.map(prefix => findPath(prefix).map(_ -> prefix))
.collectFirst { case Some((modifier, prefix)) =>
MatchResult(isExact = false, matchedPath = UpdatePath(prefix, modifier))
}
}
}
/** @return an update modifier of a matching field path, none if there is no matching field path
*/
@tailrec
final private[update] def findPath(path: List[String]): Option[UpdatePathModifier] = {
path match {
case Nil => this.modifierO
case head :: subpath if nodes.contains(head) => nodes(head).findPath(subpath)
case _ => None
}
}
}

View File

@ -0,0 +1,42 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.apiserver.update
protected[update] object UpdateRequestsPaths {
object UserPaths {
val annotations: List[String] = List(
FieldNames.UpdateUserRequest.user,
FieldNames.User.metadata,
FieldNames.Metadata.annotations,
)
val primaryParty: List[String] =
List(FieldNames.UpdateUserRequest.user, FieldNames.User.primaryParty)
val isDeactivated =
List(FieldNames.UpdateUserRequest.user, FieldNames.User.isDeactivated)
val fullUpdateTrie: UpdatePathsTrie = UpdatePathsTrie
.fromPaths(
Seq(
annotations,
primaryParty,
isDeactivated,
)
)
.getOrElse(sys.error("Failed to create full update user tree. This should never happen"))
}
object PartyDetailsPaths {
val annotations: List[String] = List(
FieldNames.UpdatePartyDetailsRequest.partyDetails,
FieldNames.User.metadata,
FieldNames.Metadata.annotations,
)
val fullUpdateTrie: UpdatePathsTrie = UpdatePathsTrie
.fromPaths(Seq(annotations))
.getOrElse(sys.error("Failed to create full update user tree. This should never happen"))
}
}

View File

@ -0,0 +1,76 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.apiserver.update
import com.daml.ledger.api.domain
import com.daml.ledger.api.domain.User
import com.daml.ledger.participant.state.index.v2._
import com.daml.lf.data.Ref
object UserUpdateMapper extends UpdateMapperBase {
import UpdateRequestsPaths.UserPaths
type DomainObject = domain.User
type UpdateObject = UserUpdate
override val fullUpdateTrie: UpdatePathsTrie = UserPaths.fullUpdateTrie
override def makeUpdateObject(user: User, updateTrie: UpdatePathsTrie): Result[UserUpdate] = {
for {
annotationsUpdate <- resolveAnnotationsUpdate(updateTrie, user.metadata.annotations)
primaryPartyUpdate <- resolvePrimaryPartyUpdate(updateTrie, user.primaryParty)
isDeactivatedUpdate <- isDeactivatedUpdateResult(updateTrie, user.isDeactivated)
} yield {
UserUpdate(
id = user.id,
primaryPartyUpdateO = primaryPartyUpdate,
isDeactivatedUpdateO = isDeactivatedUpdate,
metadataUpdate = ObjectMetaUpdate(
resourceVersionO = user.metadata.resourceVersionO,
annotationsUpdateO = annotationsUpdate,
),
)
}
}
def resolveAnnotationsUpdate(
updateTrie: UpdatePathsTrie,
newValue: Map[String, String],
): Result[Option[AnnotationsUpdate]] =
updateTrie
.findMatch(UserPaths.annotations)
.fold(noUpdate[AnnotationsUpdate])(updateMatch =>
makeAnnotationsUpdate(newValue = newValue, updateMatch = updateMatch)
)
def resolvePrimaryPartyUpdate(
updateTrie: UpdatePathsTrie,
newValue: Option[Ref.Party],
): Result[Option[Option[Ref.Party]]] =
updateTrie
.findMatch(UserPaths.primaryParty)
.fold(noUpdate[Option[Ref.Party]])(updateMatch =>
makePrimitiveFieldUpdate(
updateMatch = updateMatch,
defaultValue = None,
newValue = newValue,
)
)
def isDeactivatedUpdateResult(
updateTrie: UpdatePathsTrie,
newValue: Boolean,
): Result[Option[Boolean]] =
updateTrie
.findMatch(UserPaths.isDeactivated)
.fold(noUpdate[Boolean])(matchResult =>
makePrimitiveFieldUpdate(
updateMatch = matchResult,
defaultValue = false,
newValue = newValue,
)
)
}

View File

@ -0,0 +1,8 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.apiserver
package object update {
type Result[T] = Either[UpdatePathError, T]
}

View File

@ -0,0 +1,290 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.apiserver.update
import com.daml.ledger.api.domain.ParticipantParty.PartyRecord
import com.daml.ledger.api.domain.ObjectMeta
import com.daml.ledger.participant.state.index.v2.AnnotationsUpdate.{Merge, Replace}
import com.daml.ledger.participant.state.index.v2.{
AnnotationsUpdate,
ObjectMetaUpdate,
PartyRecordUpdate,
}
import com.daml.lf.data.Ref
import com.google.protobuf.field_mask.FieldMask
import org.scalatest.EitherValues
import org.scalatest.freespec.AnyFreeSpec
import org.scalatest.matchers.should.Matchers
class PartyRecordUpdateMapperSpec extends AnyFreeSpec with Matchers with EitherValues {
private val party1 = Ref.Party.assertFromString("party")
def makePartyRecord(
party: Ref.Party = party1,
annotations: Map[String, String] = Map.empty,
): PartyRecord = PartyRecord(
party = party,
metadata = ObjectMeta(
resourceVersionO = None,
annotations = annotations,
),
)
def makePartyRecordUpdate(
party: Ref.Party = party1,
annotationsUpdateO: Option[AnnotationsUpdate] = None,
): PartyRecordUpdate = PartyRecordUpdate(
party = party,
metadataUpdate = ObjectMetaUpdate(
resourceVersionO = None,
annotationsUpdateO = annotationsUpdateO,
),
)
val emptyUpdate: PartyRecordUpdate = makePartyRecordUpdate()
private val testedMapper = PartyRecordUpdateMapper
"map to party record updates" - {
"basic mapping" in {
val pr = makePartyRecord(annotations = Map("a" -> "b"))
val expected =
makePartyRecordUpdate(annotationsUpdateO = Some(Merge.fromNonEmpty(Map("a" -> "b"))))
testedMapper
.toUpdate(pr, FieldMask(Seq("party_details.metadata.annotations")))
.value shouldBe expected
testedMapper.toUpdate(pr, FieldMask(Seq("party_details.metadata"))).value shouldBe expected
testedMapper.toUpdate(pr, FieldMask(Seq("party_details"))).value shouldBe expected
}
"produce an empty update when new values are all default and merge update semantics is used" in {
val pr = makePartyRecord(annotations = Map.empty)
testedMapper.toUpdate(pr, FieldMask(Seq("party_details"))).value.isNoUpdate shouldBe true
}
"test use of update modifiers" - {
"when exact path match on the metadata annotations field" in {
val prWithAnnotations = makePartyRecord(annotations = Map("a" -> "b"))
val prWithoutAnnotations = makePartyRecord()
testedMapper
.toUpdate(prWithAnnotations, FieldMask(Seq("party_details.metadata.annotations!replace")))
.value shouldBe makePartyRecordUpdate(annotationsUpdateO = Some(Replace(Map("a" -> "b"))))
testedMapper
.toUpdate(
prWithoutAnnotations,
FieldMask(Seq("party_details.metadata.annotations!replace")),
)
.value shouldBe makePartyRecordUpdate(annotationsUpdateO = Some(Replace(Map.empty)))
testedMapper
.toUpdate(prWithAnnotations, FieldMask(Seq("party_details.metadata.annotations!merge")))
.value shouldBe makePartyRecordUpdate(annotationsUpdateO =
Some(Merge.fromNonEmpty(Map("a" -> "b")))
)
testedMapper
.toUpdate(
prWithoutAnnotations,
FieldMask(Seq("party_details.metadata.annotations!merge")),
)
.left
.value shouldBe UpdatePathError.MergeUpdateModifierOnEmptyMapField(
"party_details.metadata.annotations!merge"
)
testedMapper
.toUpdate(prWithAnnotations, FieldMask(Seq("party_details.metadata.annotations")))
.value shouldBe makePartyRecordUpdate(annotationsUpdateO =
Some(Merge.fromNonEmpty(Map("a" -> "b")))
)
testedMapper
.toUpdate(prWithoutAnnotations, FieldMask(Seq("party_details.metadata.annotations")))
.value shouldBe makePartyRecordUpdate(annotationsUpdateO = Some(Replace(Map.empty)))
}
"when inexact path match on metadata annotations field" in {
val prWithAnnotations = makePartyRecord(annotations = Map("a" -> "b"))
val prWithoutAnnotations = makePartyRecord()
testedMapper
.toUpdate(prWithAnnotations, FieldMask(Seq("party_details!replace")))
.value shouldBe makePartyRecordUpdate(
annotationsUpdateO = Some(Replace(Map("a" -> "b")))
)
testedMapper
.toUpdate(prWithoutAnnotations, FieldMask(Seq("party_details!replace")))
.value shouldBe makePartyRecordUpdate(
annotationsUpdateO = Some(Replace(Map.empty))
)
testedMapper
.toUpdate(prWithAnnotations, FieldMask(Seq("party_details!merge")))
.value shouldBe makePartyRecordUpdate(
annotationsUpdateO = Some(Merge.fromNonEmpty(Map("a" -> "b")))
)
testedMapper
.toUpdate(prWithoutAnnotations, FieldMask(Seq("party_details!merge")))
.value shouldBe emptyUpdate
testedMapper
.toUpdate(prWithAnnotations, FieldMask(Seq("party_details")))
.value shouldBe makePartyRecordUpdate(
annotationsUpdateO = Some(Merge.fromNonEmpty(Map("a" -> "b")))
)
testedMapper
.toUpdate(prWithoutAnnotations, FieldMask(Seq("party_details")))
.value shouldBe emptyUpdate
}
"the longest matching path is matched" in {
val pr = makePartyRecord(
annotations = Map("a" -> "b")
)
testedMapper
.toUpdate(
pr,
FieldMask(
Seq(
"party_details!replace",
"party_details.metadata!replace",
"party_details.metadata.annotations!merge",
)
),
)
.value shouldBe makePartyRecordUpdate(
annotationsUpdateO = Some(Merge.fromNonEmpty(Map("a" -> "b")))
)
testedMapper
.toUpdate(
pr,
FieldMask(
Seq(
"party_details!replace",
"party_details.metadata!replace",
"party_details.metadata.annotations",
)
),
)
.value shouldBe makePartyRecordUpdate(
annotationsUpdateO = Some(Merge.fromNonEmpty(Map("a" -> "b")))
)
testedMapper
.toUpdate(
pr,
FieldMask(
Seq(
"party_details!merge",
"party_details.metadata",
"party_details.metadata.annotations!replace",
)
),
)
.value shouldBe makePartyRecordUpdate(
annotationsUpdateO = Some(Replace(Map("a" -> "b")))
)
}
"when update modifier on a dummy field" in {
val pr = makePartyRecord(annotations = Map("a" -> "b"))
testedMapper
.toUpdate(pr, FieldMask(Seq("party_details.dummy!replace")))
.left
.value shouldBe UpdatePathError.UnknownFieldPath("party_details.dummy!replace")
}
"raise an error when an unsupported modifier like syntax is used" in {
val pr = makePartyRecord(annotations = Map("a" -> "b"))
testedMapper
.toUpdate(pr, FieldMask(Seq("party_details!badmodifier")))
.left
.value shouldBe UpdatePathError.UnknownUpdateModifier(
"party_details!badmodifier"
)
testedMapper
.toUpdate(pr, FieldMask(Seq("party_details.metadata.annotations!alsobad")))
.left
.value shouldBe UpdatePathError.UnknownUpdateModifier(
"party_details.metadata.annotations!alsobad"
)
}
}
}
"produce an error when " - {
val pr = makePartyRecord(annotations = Map("a" -> "b"))
"field masks lists unknown field" in {
testedMapper
.toUpdate(pr, FieldMask(Seq("some_unknown_field")))
.left
.value shouldBe UpdatePathError.UnknownFieldPath("some_unknown_field")
testedMapper
.toUpdate(pr, FieldMask(Seq("party_details", "some_unknown_field")))
.left
.value shouldBe UpdatePathError.UnknownFieldPath("some_unknown_field")
testedMapper
.toUpdate(pr, FieldMask(Seq("party_details", "party_details.some_unknown_field")))
.left
.value shouldBe UpdatePathError.UnknownFieldPath("party_details.some_unknown_field")
}
"attempting to update resource version" in {
testedMapper
.toUpdate(pr, FieldMask(Seq("party_details.metadata.resource_version")))
.left
.value shouldBe UpdatePathError.UnknownFieldPath("party_details.metadata.resource_version")
}
"empty string update path" in {
testedMapper
.toUpdate(pr, FieldMask(Seq("")))
.left
.value shouldBe UpdatePathError.EmptyFieldPath("")
}
"empty string field path part of the field mask but non-empty update modifier" in {
testedMapper
.toUpdate(pr, FieldMask(Seq("!merge")))
.left
.value shouldBe UpdatePathError.EmptyFieldPath("!merge")
}
"empty field mask" in {
testedMapper
.toUpdate(pr, FieldMask(Seq.empty))
.left
.value shouldBe UpdatePathError.EmptyFieldMask
}
"update path with invalid field path syntax" in {
testedMapper
.toUpdate(pr, FieldMask(Seq("party_details..metadata")))
.left
.value shouldBe UpdatePathError.UnknownFieldPath("party_details..metadata")
testedMapper
.toUpdate(pr, FieldMask(Seq(".party_details.metadata")))
.left
.value shouldBe UpdatePathError.UnknownFieldPath(".party_details.metadata")
testedMapper
.toUpdate(pr, FieldMask(Seq(".party_details!merge.metadata")))
.left
.value shouldBe UpdatePathError.UnknownUpdateModifier(".party_details!merge.metadata")
testedMapper
.toUpdate(pr, FieldMask(Seq("party_details!merge.metadata!merge")))
.left
.value shouldBe UpdatePathError.InvalidUpdatePathSyntax(
"party_details!merge.metadata!merge"
)
}
"multiple update paths with the same field path" in {
testedMapper
.toUpdate(
pr,
FieldMask(Seq("party_details.metadata!merge", "party_details.metadata!replace")),
)
.left
.value shouldBe UpdatePathError.DuplicatedFieldPath("party_details.metadata!replace")
testedMapper
.toUpdate(
pr,
FieldMask(
Seq("party_details.metadata.annotations!merge", "party_details.metadata.annotations")
),
)
.left
.value shouldBe UpdatePathError.DuplicatedFieldPath("party_details.metadata.annotations")
}
}
}

View File

@ -0,0 +1,46 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.apiserver.update
import org.scalatest.EitherValues
import org.scalatest.freespec.AnyFreeSpec
import org.scalatest.matchers.should.Matchers
class UpdatePathSpec extends AnyFreeSpec with Matchers with EitherValues {
"parse valid update paths" in {
UpdatePath
.parseAll(
Seq(
"foo.bar",
"foo",
"foo.bar!merge",
"foo.bar!replace",
)
)
.value shouldBe Seq(
UpdatePath(List("foo", "bar"), UpdatePathModifier.NoModifier),
UpdatePath(List("foo"), UpdatePathModifier.NoModifier),
UpdatePath(List("foo", "bar"), UpdatePathModifier.Merge),
UpdatePath(List("foo", "bar"), UpdatePathModifier.Replace),
)
}
"raise errors when parsing invalid paths" in {
UpdatePath.parseAll(Seq("")).left.value shouldBe UpdatePathError.EmptyFieldPath("")
UpdatePath.parseAll(Seq("!merge")).left.value shouldBe UpdatePathError.EmptyFieldPath("!merge")
UpdatePath.parseAll(Seq("!replace")).left.value shouldBe UpdatePathError.EmptyFieldPath(
"!replace"
)
UpdatePath.parseAll(Seq("!bad")).left.value shouldBe UpdatePathError.EmptyFieldPath("!bad")
UpdatePath.parseAll(Seq("foo!bad")).left.value shouldBe UpdatePathError.UnknownUpdateModifier(
"foo!bad"
)
UpdatePath.parseAll(Seq("foo!!bad")).left.value shouldBe UpdatePathError
.InvalidUpdatePathSyntax("foo!!bad")
UpdatePath.parseAll(Seq("!")).left.value shouldBe UpdatePathError.EmptyFieldPath("!")
UpdatePath.parseAll(Seq("!!")).left.value shouldBe UpdatePathError.EmptyFieldPath("!!")
}
}

View File

@ -0,0 +1,153 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.apiserver.update
import com.daml.error.ErrorsAssertions
import org.scalatest.{EitherValues, OptionValues}
import org.scalatest.freespec.AnyFreeSpec
import org.scalatest.matchers.should.Matchers
import com.daml.platform.apiserver.update.UpdatePathModifier._
class UpdatePathsTrieSpec
extends AnyFreeSpec
with Matchers
with EitherValues
with OptionValues
with ErrorsAssertions {
"finding update paths in" - {
val allPaths = UpdatePath
.parseAll(
Seq(
"a1.a2.c3",
"a1.b2.c3",
"a1.b2.d3.a4",
"a1.b2.d3.b4",
"b1",
)
)
.value
val tree = UpdatePathsTrie.fromPaths(allPaths).value
"proper subtrees" in {
tree.findPath(List("a1")) shouldBe None
tree.findPath(List.empty) shouldBe None
tree.findPath(List("a1", "b2")) shouldBe None
}
"non-existing subtrees" in {
tree.findPath(List("a1", "b2", "dummy")) shouldBe None
tree.findPath(List("dummy")) shouldBe None
tree.findPath(List("")) shouldBe None
}
"existing but empty subtrees" in {
tree.findPath(List("b1")) shouldBe Some(UpdatePathModifier.NoModifier)
tree.findPath(List("a1", "b2", "c3")) shouldBe Some(UpdatePathModifier.NoModifier)
tree.findPath(List("a1", "b2", "d3", "b4")) shouldBe Some(UpdatePathModifier.NoModifier)
}
}
"constructing a trie" - {
"from one path with one segment" in {
UpdatePathsTrie
.fromPaths(
UpdatePath.parseAll(Seq("foo")).value
)
.value shouldBe UpdatePathsTrie(
None,
"foo" -> UpdatePathsTrie(Some(NoModifier)),
)
}
"from one path with multiple segments" in {
UpdatePathsTrie
.fromPaths(
UpdatePath.parseAll(Seq("foo.bar.baz")).value
)
.value shouldBe UpdatePathsTrie(
None,
"foo" -> UpdatePathsTrie(
None,
"bar" -> UpdatePathsTrie(
None,
"baz" -> UpdatePathsTrie(
Some(NoModifier)
),
),
),
)
}
"from three paths with multiple segments and with update modifiers" in {
val t = UpdatePathsTrie
.fromPaths(
UpdatePath
.parseAll(
Seq(
"foo.bar.baz",
"foo.bar!merge",
"foo.alice",
"bob.eve",
"bob!replace",
)
)
.value
)
.value
t shouldBe UpdatePathsTrie(
None,
"foo" -> UpdatePathsTrie(
None,
"bar" -> UpdatePathsTrie(
Some(Merge),
"baz" -> UpdatePathsTrie(Some(NoModifier)),
),
"alice" -> UpdatePathsTrie(Some(NoModifier)),
),
"bob" -> UpdatePathsTrie(
Some(Replace),
"eve" -> UpdatePathsTrie(Some(NoModifier)),
),
)
}
}
"checking for presence of a prefix" in {
val t = UpdatePathsTrie
.fromPaths(
UpdatePath
.parseAll(
Seq(
"foo.bar.baz",
"foo.bax",
)
)
.value
)
.value
t.containsPrefix(List("foo")) shouldBe true
t.containsPrefix(List("foo", "bar")) shouldBe true
t.containsPrefix(List("foo", "bax")) shouldBe true
t.containsPrefix(List("foo", "bar", "baz")) shouldBe true
t.containsPrefix(List("foo", "bar", "bad")) shouldBe false
t.containsPrefix(List("")) shouldBe false
t.containsPrefix(List.empty) shouldBe true
}
"fail to build a trie when duplicated field paths" in {
UpdatePathsTrie
.fromPaths(
UpdatePath
.parseAll(
Seq(
"foo.bar",
"foo.bar!merge",
)
)
.value
)
.left
.value shouldBe UpdatePathError.DuplicatedFieldPath("foo.bar!merge")
}
}

View File

@ -0,0 +1,406 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.apiserver.update
import com.daml.ledger.api.domain.{ObjectMeta, User}
import com.daml.ledger.participant.state.index.v2.{AnnotationsUpdate, ObjectMetaUpdate, UserUpdate}
import com.daml.ledger.participant.state.index.v2.AnnotationsUpdate.{Merge, Replace}
import com.daml.lf.data.Ref
import com.google.protobuf.field_mask.FieldMask
import org.scalatest.EitherValues
import org.scalatest.freespec.AnyFreeSpec
import org.scalatest.matchers.should.Matchers
class UserUpdateMapperSpec extends AnyFreeSpec with Matchers with EitherValues {
private val userId1: Ref.UserId = Ref.UserId.assertFromString("u1")
private val party1 = Ref.Party.assertFromString("party")
def makeUser(
id: Ref.UserId = userId1,
primaryParty: Option[Ref.Party] = None,
isDeactivated: Boolean = false,
annotations: Map[String, String] = Map.empty,
): User = User(
id = id,
primaryParty = primaryParty,
isDeactivated = isDeactivated,
metadata = ObjectMeta(
resourceVersionO = None,
annotations = annotations,
),
)
def makeUserUpdate(
id: Ref.UserId = userId1,
primaryPartyUpdateO: Option[Option[Ref.Party]] = None,
isDeactivatedUpdateO: Option[Boolean] = None,
annotationsUpdateO: Option[AnnotationsUpdate] = None,
): UserUpdate = UserUpdate(
id = id,
primaryPartyUpdateO = primaryPartyUpdateO,
isDeactivatedUpdateO = isDeactivatedUpdateO,
metadataUpdate = ObjectMetaUpdate(
resourceVersionO = None,
annotationsUpdateO = annotationsUpdateO,
),
)
val emptyUserUpdate: UserUpdate = makeUserUpdate()
"map to user updates" - {
"basic mapping" - {
val user = makeUser(
primaryParty = None,
isDeactivated = false,
annotations = Map("a" -> "b"),
)
val expected = makeUserUpdate(
primaryPartyUpdateO = Some(None),
isDeactivatedUpdateO = Some(false),
annotationsUpdateO = Some(Merge.fromNonEmpty(Map("a" -> "b"))),
)
"1) with all individual fields to update listed in the update mask" in {
UserUpdateMapper
.toUpdate(
user,
FieldMask(Seq("user.is_deactivated", "user.primary_party", "user.metadata.annotations")),
)
.value shouldBe expected
}
"2) with metadata.annotations not listed explicitly" in {
UserUpdateMapper
.toUpdate(
user,
FieldMask(Seq("user.is_deactivated", "user.primary_party", "user.metadata")),
)
.value shouldBe expected
}
}
"map api request to update - merge user and reset is_deactivated" - {
val user = makeUser(
// non-default value
primaryParty = Some(party1),
// default value
isDeactivated = false,
// non-default value
annotations = Map("a" -> "b"),
)
val expected = makeUserUpdate(
primaryPartyUpdateO = Some(Some(party1)),
isDeactivatedUpdateO = Some(false),
annotationsUpdateO = Some(Merge.fromNonEmpty(Map("a" -> "b"))),
)
"1) minimal field mask" in {
UserUpdateMapper
.toUpdate(user, FieldMask(Seq("user", "user.is_deactivated")))
.value shouldBe expected
}
"2) not so minimal field mask" in {
UserUpdateMapper
.toUpdate(user, FieldMask(Seq("user", "user.metadata", "user.is_deactivated")))
.value shouldBe expected
}
"3) also not so minimal field mask" in {
UserUpdateMapper
.toUpdate(
user,
FieldMask(Seq("user.primary_party", "user.metadata", "user.is_deactivated")),
)
.value shouldBe expected
}
"4) field mask with exact field paths" in {
UserUpdateMapper
.toUpdate(
user,
FieldMask(Seq("user.primary_party", "user.metadata.annotations", "user.is_deactivated")),
)
.value shouldBe expected
}
}
// TODO um-for-hub major: Document that no-up update requests are invalid
"produce an empty update when new values are all default and merge update semantics is used" in {
val user = makeUser(
primaryParty = None,
isDeactivated = false,
annotations = Map.empty,
)
UserUpdateMapper
.toUpdate(
user,
FieldMask(
Seq(
"user"
)
),
)
.value
.isNoUpdate shouldBe true
}
"test use of update modifiers" - {
"when exact path match on a primitive field" in {
val userWithParty = makeUser(primaryParty = Some(party1))
val userWithoutParty = makeUser()
UserUpdateMapper
.toUpdate(userWithParty, FieldMask(Seq("user.primary_party!replace")))
.value shouldBe makeUserUpdate(primaryPartyUpdateO = Some(Some(party1)))
UserUpdateMapper
.toUpdate(userWithoutParty, FieldMask(Seq("user.primary_party!replace")))
.value shouldBe makeUserUpdate(primaryPartyUpdateO = Some(None))
UserUpdateMapper
.toUpdate(userWithParty, FieldMask(Seq("user.primary_party!merge")))
.value shouldBe makeUserUpdate(primaryPartyUpdateO = Some(Some(party1)))
UserUpdateMapper
.toUpdate(userWithoutParty, FieldMask(Seq("user.primary_party!merge")))
.left
.value shouldBe UpdatePathError.MergeUpdateModifierOnPrimitiveField(
"user.primary_party!merge"
)
UserUpdateMapper
.toUpdate(userWithParty, FieldMask(Seq("user.primary_party")))
.value shouldBe makeUserUpdate(primaryPartyUpdateO = Some(Some(party1)))
UserUpdateMapper
.toUpdate(userWithoutParty, FieldMask(Seq("user.primary_party")))
.value shouldBe makeUserUpdate(primaryPartyUpdateO = Some(None))
}
"when exact path match on the metadata annotations field" in {
val userWithAnnotations = makeUser(annotations = Map("a" -> "b"))
val userWithoutAnnotations = makeUser()
UserUpdateMapper
.toUpdate(userWithAnnotations, FieldMask(Seq("user.metadata.annotations!replace")))
.value shouldBe makeUserUpdate(annotationsUpdateO = Some(Replace(Map("a" -> "b"))))
UserUpdateMapper
.toUpdate(userWithoutAnnotations, FieldMask(Seq("user.metadata.annotations!replace")))
.value shouldBe makeUserUpdate(annotationsUpdateO = Some(Replace(Map.empty)))
UserUpdateMapper
.toUpdate(userWithAnnotations, FieldMask(Seq("user.metadata.annotations!merge")))
.value shouldBe makeUserUpdate(annotationsUpdateO =
Some(Merge.fromNonEmpty(Map("a" -> "b")))
)
UserUpdateMapper
.toUpdate(userWithoutAnnotations, FieldMask(Seq("user.metadata.annotations!merge")))
.left
.value shouldBe UpdatePathError.MergeUpdateModifierOnEmptyMapField(
"user.metadata.annotations!merge"
)
UserUpdateMapper
.toUpdate(userWithAnnotations, FieldMask(Seq("user.metadata.annotations")))
.value shouldBe makeUserUpdate(annotationsUpdateO =
Some(Merge.fromNonEmpty(Map("a" -> "b")))
)
UserUpdateMapper
.toUpdate(userWithoutAnnotations, FieldMask(Seq("user.metadata.annotations")))
.value shouldBe makeUserUpdate(annotationsUpdateO = Some(Replace(Map.empty)))
}
"when inexact path match for a primitive field" in {
val userWithParty = makeUser(primaryParty = Some(party1))
val userWithoutParty = makeUser()
UserUpdateMapper
.toUpdate(userWithParty, FieldMask(Seq("user!replace")))
.value shouldBe makeUserUpdate(
primaryPartyUpdateO = Some(Some(party1)),
isDeactivatedUpdateO = Some(false),
annotationsUpdateO = Some(Replace(Map.empty)),
)
UserUpdateMapper
.toUpdate(userWithoutParty, FieldMask(Seq("user!replace")))
.value shouldBe makeUserUpdate(
primaryPartyUpdateO = Some(None),
isDeactivatedUpdateO = Some(false),
annotationsUpdateO = Some(Replace(Map.empty)),
)
UserUpdateMapper
.toUpdate(userWithParty, FieldMask(Seq("user!merge")))
.value shouldBe makeUserUpdate(primaryPartyUpdateO = Some(Some(party1)))
UserUpdateMapper
.toUpdate(userWithoutParty, FieldMask(Seq("user!merge")))
.value shouldBe emptyUserUpdate
UserUpdateMapper
.toUpdate(userWithParty, FieldMask(Seq("user")))
.value shouldBe makeUserUpdate(primaryPartyUpdateO = Some(Some(party1)))
UserUpdateMapper
.toUpdate(userWithoutParty, FieldMask(Seq("user")))
.value shouldBe emptyUserUpdate
}
"when inexact path match on metadata annotations field" in {
val userWithAnnotations = makeUser(annotations = Map("a" -> "b"))
val userWithoutAnnotations = makeUser()
UserUpdateMapper
.toUpdate(userWithAnnotations, FieldMask(Seq("user!replace")))
.value shouldBe makeUserUpdate(
primaryPartyUpdateO = Some(None),
isDeactivatedUpdateO = Some(false),
annotationsUpdateO = Some(Replace(Map("a" -> "b"))),
)
UserUpdateMapper
.toUpdate(userWithoutAnnotations, FieldMask(Seq("user!replace")))
.value shouldBe makeUserUpdate(
primaryPartyUpdateO = Some(None),
isDeactivatedUpdateO = Some(false),
annotationsUpdateO = Some(Replace(Map.empty)),
)
UserUpdateMapper
.toUpdate(userWithAnnotations, FieldMask(Seq("user!merge")))
.value shouldBe makeUserUpdate(
annotationsUpdateO = Some(Merge.fromNonEmpty(Map("a" -> "b")))
)
UserUpdateMapper
.toUpdate(userWithoutAnnotations, FieldMask(Seq("user!merge")))
.value shouldBe emptyUserUpdate
UserUpdateMapper
.toUpdate(userWithAnnotations, FieldMask(Seq("user")))
.value shouldBe makeUserUpdate(
annotationsUpdateO = Some(Merge.fromNonEmpty(Map("a" -> "b")))
)
UserUpdateMapper
.toUpdate(userWithoutAnnotations, FieldMask(Seq("user")))
.value shouldBe emptyUserUpdate
}
"the longest matching path is matched" in {
val user = makeUser(
annotations = Map("a" -> "b"),
primaryParty = Some(party1),
)
UserUpdateMapper
.toUpdate(
user,
FieldMask(
Seq("user!replace", "user.metadata!replace", "user.metadata.annotations!merge")
),
)
.value shouldBe makeUserUpdate(
primaryPartyUpdateO = Some(Some(party1)),
isDeactivatedUpdateO = Some(false),
annotationsUpdateO = Some(Merge.fromNonEmpty(Map("a" -> "b"))),
)
UserUpdateMapper
.toUpdate(
user,
FieldMask(Seq("user!replace", "user.metadata!replace", "user.metadata.annotations")),
)
.value shouldBe makeUserUpdate(
primaryPartyUpdateO = Some(Some(party1)),
isDeactivatedUpdateO = Some(false),
annotationsUpdateO = Some(Merge.fromNonEmpty(Map("a" -> "b"))),
)
UserUpdateMapper
.toUpdate(
user,
FieldMask(Seq("user!merge", "user.metadata", "user.metadata.annotations!replace")),
)
.value shouldBe makeUserUpdate(
primaryPartyUpdateO = Some(Some(party1)),
annotationsUpdateO = Some(Replace(Map("a" -> "b"))),
)
}
"when update modifier on a dummy field" in {
val user = makeUser(primaryParty = Some(party1))
UserUpdateMapper
.toUpdate(user, FieldMask(Seq("user.dummy!replace")))
.left
.value shouldBe UpdatePathError.UnknownFieldPath("user.dummy!replace")
}
"raise an error when an unsupported modifier like syntax is used" in {
val user = makeUser(primaryParty = Some(party1))
UserUpdateMapper
.toUpdate(user, FieldMask(Seq("user!badmodifier")))
.left
.value shouldBe UpdatePathError.UnknownUpdateModifier(
"user!badmodifier"
)
UserUpdateMapper
.toUpdate(user, FieldMask(Seq("user.metadata.annotations!alsobad")))
.left
.value shouldBe UpdatePathError.UnknownUpdateModifier(
"user.metadata.annotations!alsobad"
)
}
}
}
"produce an error when " - {
val user = makeUser(primaryParty = Some(party1))
"field masks lists unknown field" in {
UserUpdateMapper
.toUpdate(user, FieldMask(Seq("some_unknown_field")))
.left
.value shouldBe UpdatePathError.UnknownFieldPath("some_unknown_field")
UserUpdateMapper
.toUpdate(user, FieldMask(Seq("user", "some_unknown_field")))
.left
.value shouldBe UpdatePathError.UnknownFieldPath("some_unknown_field")
UserUpdateMapper
.toUpdate(user, FieldMask(Seq("user", "user.some_unknown_field")))
.left
.value shouldBe UpdatePathError.UnknownFieldPath("user.some_unknown_field")
}
"attempting to update resource version" in {
UserUpdateMapper
.toUpdate(user, FieldMask(Seq("user.metadata.resource_version")))
.left
.value shouldBe UpdatePathError.UnknownFieldPath("user.metadata.resource_version")
}
"empty string update path" in {
UserUpdateMapper
.toUpdate(user, FieldMask(Seq("")))
.left
.value shouldBe UpdatePathError.EmptyFieldPath("")
}
"empty string field path part of the field mask but non-empty update modifier" in {
UserUpdateMapper
.toUpdate(user, FieldMask(Seq("!merge")))
.left
.value shouldBe UpdatePathError.EmptyFieldPath("!merge")
}
"empty field mask" in {
UserUpdateMapper
.toUpdate(user, FieldMask(Seq.empty))
.left
.value shouldBe UpdatePathError.EmptyFieldMask
}
"update path with invalid field path syntax" in {
UserUpdateMapper
.toUpdate(user, FieldMask(Seq("user..primary_party")))
.left
.value shouldBe UpdatePathError.UnknownFieldPath("user..primary_party")
UserUpdateMapper
.toUpdate(user, FieldMask(Seq(".user.primary_party")))
.left
.value shouldBe UpdatePathError.UnknownFieldPath(".user.primary_party")
UserUpdateMapper
.toUpdate(user, FieldMask(Seq(".user!merge.primary_party")))
.left
.value shouldBe UpdatePathError.UnknownUpdateModifier(".user!merge.primary_party")
UserUpdateMapper
.toUpdate(user, FieldMask(Seq("user!merge.primary_party!merge")))
.left
.value shouldBe UpdatePathError.InvalidUpdatePathSyntax(
"user!merge.primary_party!merge"
)
}
"multiple update paths with the same field path" in {
UserUpdateMapper
.toUpdate(
user,
FieldMask(Seq("user.primary_party!merge", "user.primary_party!replace")),
)
.left
.value shouldBe UpdatePathError.DuplicatedFieldPath("user.primary_party!replace")
UserUpdateMapper
.toUpdate(user, FieldMask(Seq("user.primary_party!merge", "user.primary_party")))
.left
.value shouldBe UpdatePathError.DuplicatedFieldPath("user.primary_party")
}
}
}

View File

@ -19,7 +19,9 @@ trait LedgerPartyExists {
case class PartyRecordUpdate(
party: Ref.Party,
metadataUpdate: ObjectMetaUpdate,
)
) {
def isNoUpdate: Boolean = metadataUpdate.isNoUpdate
}
trait PartyRecordStore {
import PartyRecordStore._

View File

@ -14,7 +14,10 @@ case class UserUpdate(
primaryPartyUpdateO: Option[Option[Ref.Party]] = None,
isDeactivatedUpdateO: Option[Boolean] = None,
metadataUpdate: ObjectMetaUpdate,
)
) {
def isNoUpdate: Boolean =
primaryPartyUpdateO.isEmpty && isDeactivatedUpdateO.isEmpty && metadataUpdate.isNoUpdate
}
sealed trait AnnotationsUpdate {
def annotations: Map[String, String]
@ -48,7 +51,9 @@ object AnnotationsUpdate {
case class ObjectMetaUpdate(
resourceVersionO: Option[Long],
annotationsUpdateO: Option[AnnotationsUpdate],
)
) {
def isNoUpdate: Boolean = annotationsUpdateO.isEmpty
}
object ObjectMetaUpdate {
def empty: ObjectMetaUpdate = ObjectMetaUpdate(
resourceVersionO = None,