mirror of
https://github.com/digital-asset/daml.git
synced 2024-11-10 10:46:11 +03:00
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:
parent
1effd3eab2
commit
0853fc44e0
@ -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"
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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"))
|
||||
}
|
||||
|
||||
}
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
}
|
@ -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]
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
@ -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("!!")
|
||||
}
|
||||
|
||||
}
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
@ -19,7 +19,9 @@ trait LedgerPartyExists {
|
||||
case class PartyRecordUpdate(
|
||||
party: Ref.Party,
|
||||
metadataUpdate: ObjectMetaUpdate,
|
||||
)
|
||||
) {
|
||||
def isNoUpdate: Boolean = metadataUpdate.isNoUpdate
|
||||
}
|
||||
|
||||
trait PartyRecordStore {
|
||||
import PartyRecordStore._
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user