mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 01:07:18 +03:00
LF: provide methods to enrich LF values missing type and field names (#8493)
CHANGELOG_BEGIN CHANGELOG_END
This commit is contained in:
parent
89bc670445
commit
77bec01db3
@ -19,6 +19,7 @@ import java.nio.file.Files
|
||||
|
||||
import com.daml.lf.language.LanguageVersion
|
||||
import com.daml.lf.validation.Validation
|
||||
import com.daml.lf.value.Value.ContractId
|
||||
|
||||
/** Allows for evaluating [[Commands]] and validating [[Transaction]]s.
|
||||
* <p>
|
||||
@ -217,7 +218,7 @@ class Engine(val config: EngineConfig = new EngineConfig(LanguageVersion.StableV
|
||||
} yield validationResult
|
||||
}
|
||||
|
||||
private def loadPackages(pkgIds: List[PackageId]): Result[Unit] =
|
||||
private[engine] def loadPackages(pkgIds: List[PackageId]): Result[Unit] =
|
||||
pkgIds.dropWhile(compiledPackages.signatures.isDefinedAt) match {
|
||||
case pkgId :: rest =>
|
||||
ResultNeedPackage(
|
||||
@ -447,6 +448,9 @@ class Engine(val config: EngineConfig = new EngineConfig(LanguageVersion.StableV
|
||||
} yield ()
|
||||
}
|
||||
|
||||
private[engine] def enrich(typ: Type, value: Value[ContractId]): Result[Value[ContractId]] =
|
||||
preprocessor.translateValue(typ, value).map(_.toValue)
|
||||
|
||||
}
|
||||
|
||||
object Engine {
|
||||
|
@ -0,0 +1,152 @@
|
||||
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.lf
|
||||
package engine
|
||||
|
||||
import com.daml.lf.data.Ref.{Identifier, Name, PackageId}
|
||||
import com.daml.lf.language.Ast
|
||||
import com.daml.lf.language.Ast.PackageSignature
|
||||
import com.daml.lf.transaction.Node.{GenNode, KeyWithMaintainers}
|
||||
import com.daml.lf.transaction.{CommittedTransaction, Node, NodeId, VersionedTransaction}
|
||||
import com.daml.lf.value.Value
|
||||
import com.daml.lf.value.Value.ContractId
|
||||
|
||||
// Provide methods to add missing information in values (and value containers):
|
||||
// - type constructor in records, variants, and enums
|
||||
// - Records' field names
|
||||
final class ValueEnricher(engine: Engine) {
|
||||
|
||||
private[this] def getPackage(pkgId: PackageId): Result[PackageSignature] = {
|
||||
val signature = engine.compiledPackages().signatures
|
||||
if (signature.isDefinedAt(pkgId)) {
|
||||
ResultDone(signature(pkgId))
|
||||
} else {
|
||||
engine.loadPackages(List(pkgId)).map(_ => signature(pkgId))
|
||||
}
|
||||
}
|
||||
|
||||
def enrichValue(typ: Ast.Type, value: Value[ContractId]): Result[Value[ContractId]] =
|
||||
engine.enrich(typ, value)
|
||||
|
||||
def enrichContract(
|
||||
contract: Value.ContractInst[Value[ContractId]]
|
||||
): Result[Value.ContractInst[Value[ContractId]]] =
|
||||
for {
|
||||
arg <- enrichContract(contract.template, contract.arg)
|
||||
} yield contract.copy(arg = arg)
|
||||
|
||||
def enrichContract(tyCon: Identifier, value: Value[ContractId]): Result[Value[ContractId]] =
|
||||
enrichValue(Ast.TTyCon(tyCon), value)
|
||||
|
||||
def enrichChoiceArgument(
|
||||
tyCon: Identifier,
|
||||
choiceName: Name,
|
||||
value: Value[ContractId],
|
||||
): Result[Value[ContractId]] =
|
||||
for {
|
||||
pkg <- getPackage(tyCon.packageId)
|
||||
tmpl <- Result.fromEither(SignatureLookup.lookupTemplate(pkg, tyCon.qualifiedName))
|
||||
enrichedValue <- tmpl.choices.get(choiceName) match {
|
||||
case Some(choice) =>
|
||||
enrichValue(choice.argBinder._2, value)
|
||||
case None =>
|
||||
ResultError(Error(s"choice $tyCon:$choiceName not found"))
|
||||
}
|
||||
} yield enrichedValue
|
||||
|
||||
def enrichChoiceResult(
|
||||
tyCon: Identifier,
|
||||
choiceName: Name,
|
||||
value: Value[ContractId],
|
||||
): Result[Value[ContractId]] =
|
||||
for {
|
||||
pkg <- getPackage(tyCon.packageId)
|
||||
tmpl <- Result.fromEither(SignatureLookup.lookupTemplate(pkg, tyCon.qualifiedName))
|
||||
enrichedValue <- tmpl.choices.get(choiceName) match {
|
||||
case Some(choice) =>
|
||||
enrichValue(choice.returnType, value)
|
||||
case None =>
|
||||
ResultError(Error(s"choice $tyCon:$choiceName not found"))
|
||||
}
|
||||
} yield enrichedValue
|
||||
|
||||
def enrichContractKey(tyCon: Identifier, value: Value[ContractId]): Result[Value[ContractId]] =
|
||||
for {
|
||||
pkg <- getPackage(tyCon.packageId)
|
||||
tmpl <- Result.fromEither(SignatureLookup.lookupTemplate(pkg, tyCon.qualifiedName))
|
||||
enrichedValue <- tmpl.key match {
|
||||
case Some(contractKey) =>
|
||||
enrichValue(contractKey.typ, value)
|
||||
case None =>
|
||||
ResultError(Error(s"template $tyCon does not have contract Key"))
|
||||
}
|
||||
} yield enrichedValue
|
||||
|
||||
private val ResultNone = ResultDone(None)
|
||||
|
||||
def enrichContractKey(
|
||||
tyCon: Identifier,
|
||||
key: KeyWithMaintainers[Value[ContractId]],
|
||||
): Result[KeyWithMaintainers[Value[ContractId]]] =
|
||||
enrichContractKey(tyCon, key.key).map(normalizedKey => key.copy(key = normalizedKey))
|
||||
|
||||
def enrichContractKey(
|
||||
tyCon: Identifier,
|
||||
key: Option[KeyWithMaintainers[Value[ContractId]]],
|
||||
): Result[Option[KeyWithMaintainers[Value[ContractId]]]] =
|
||||
key match {
|
||||
case Some(k) =>
|
||||
enrichContractKey(tyCon, k).map(Some(_))
|
||||
case None =>
|
||||
ResultNone
|
||||
}
|
||||
|
||||
def enrichNode[Nid](node: GenNode[Nid, ContractId]): Result[GenNode[Nid, ContractId]] =
|
||||
node match {
|
||||
case create: Node.NodeCreate[ContractId] =>
|
||||
for {
|
||||
contractInstance <- enrichContract(create.coinst)
|
||||
key <- enrichContractKey(create.templateId, create.key)
|
||||
} yield create.copy(coinst = contractInstance, key = key)
|
||||
case fetch: Node.NodeFetch[ContractId] =>
|
||||
for {
|
||||
key <- enrichContractKey(fetch.templateId, fetch.key)
|
||||
} yield fetch.copy(key = key)
|
||||
case lookup: Node.NodeLookupByKey[ContractId] =>
|
||||
for {
|
||||
key <- enrichContractKey(lookup.templateId, lookup.key)
|
||||
} yield lookup.copy(key = key)
|
||||
case exe: Node.NodeExercises[Nid, ContractId] =>
|
||||
for {
|
||||
choiceArg <- enrichChoiceArgument(exe.templateId, exe.choiceId, exe.chosenValue)
|
||||
result <- exe.exerciseResult match {
|
||||
case Some(exeResult) =>
|
||||
enrichChoiceResult(exe.templateId, exe.choiceId, exeResult).map(Some(_))
|
||||
case None =>
|
||||
ResultNone
|
||||
}
|
||||
key <- enrichContractKey(exe.templateId, exe.key)
|
||||
} yield exe.copy(chosenValue = choiceArg, exerciseResult = result, key = key)
|
||||
}
|
||||
|
||||
def enrichTransaction(tx: CommittedTransaction): Result[CommittedTransaction] = {
|
||||
for {
|
||||
normalizedNodes <-
|
||||
tx.nodes.foldLeft[Result[Map[NodeId, GenNode[NodeId, ContractId]]]](ResultDone(Map.empty)) {
|
||||
case (acc, (nid, node)) =>
|
||||
for {
|
||||
nodes <- acc
|
||||
normalizedNode <- enrichNode(node)
|
||||
} yield nodes.updated(nid, normalizedNode)
|
||||
}
|
||||
} yield CommittedTransaction(
|
||||
VersionedTransaction(
|
||||
version = tx.version,
|
||||
nodes = normalizedNodes,
|
||||
roots = tx.roots,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,244 @@
|
||||
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.lf
|
||||
package engine
|
||||
|
||||
import com.daml.lf.data._
|
||||
import com.daml.lf.language.Ast.{TNat, TTyCon}
|
||||
import com.daml.lf.language.Util._
|
||||
import com.daml.lf.testing.parser.Implicits._
|
||||
import com.daml.lf.transaction.TransactionVersion
|
||||
import com.daml.lf.transaction.test.TransactionBuilder
|
||||
import com.daml.lf.value.Value
|
||||
import com.daml.lf.value.Value._
|
||||
import org.scalatest.prop.TableDrivenPropertyChecks
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
|
||||
import scala.language.implicitConversions
|
||||
|
||||
class ValueEnricherSpec extends AnyWordSpec with Matchers with TableDrivenPropertyChecks {
|
||||
|
||||
import defaultParserParameters.{defaultPackageId => pkgId}
|
||||
|
||||
private implicit def toName(s: String): Ref.Name = Ref.Name.assertFromString(s)
|
||||
|
||||
val pkg =
|
||||
p"""
|
||||
module Mod {
|
||||
|
||||
record @serializable Record = { field : Int64 };
|
||||
variant @serializable Variant = variant1 : Text | variant2 : Int64 ;
|
||||
enum Enum = value1 | value2;
|
||||
|
||||
record @serializable Key = {
|
||||
party: Party,
|
||||
idx: Int64
|
||||
};
|
||||
|
||||
record @serializable Contract = {
|
||||
key: Mod:Key,
|
||||
cids: List (ContractId Mod:Contract)
|
||||
};
|
||||
|
||||
val @noPartyLiterals keyParties: (Mod:Key -> List Party) =
|
||||
\(key: Mod:Key) ->
|
||||
Cons @Party [Mod:Key {party} key] (Nil @Party);
|
||||
|
||||
val @noPartyLiterals contractParties : (Mod:Contract -> List Party) =
|
||||
\(contract: Mod:Contract) ->
|
||||
Mod:keyParties (Mod:Contract {key} contract);
|
||||
|
||||
template (this : Contract) = {
|
||||
precondition True,
|
||||
signatories Mod:contractParties this,
|
||||
observers Mod:contractParties this,
|
||||
agreement "Agreement",
|
||||
choices {
|
||||
choice @nonConsuming Noop (self) (r: Mod:Record) : Mod:Record,
|
||||
controllers
|
||||
Mod:contractParties this
|
||||
to
|
||||
upure @Mod:Record r
|
||||
},
|
||||
key @Mod:Key (Mod:Contract {key} this) Mod:keyParties
|
||||
};
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
val recordCon = Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Mod:Record"))
|
||||
val variantCon = Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Mod:Variant"))
|
||||
val enumCon = Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Mod:Enum"))
|
||||
|
||||
val contractCon = Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Mod:Contract"))
|
||||
val keyCon = Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Mod:Key"))
|
||||
|
||||
private[this] val engine = Engine.DevEngine()
|
||||
engine.preloadPackage(pkgId, pkg).consume(_ => None, _ => None, _ => None)
|
||||
|
||||
private[this] val enricher = new ValueEnricher(engine)
|
||||
|
||||
"enrichValue" should {
|
||||
|
||||
val testCases = Table(
|
||||
("type", "input", "expected output"),
|
||||
(TUnit, ValueUnit, ValueUnit),
|
||||
(TBool, ValueTrue, ValueTrue),
|
||||
(TInt64, ValueInt64(42), ValueInt64(42)),
|
||||
(
|
||||
TTimestamp,
|
||||
ValueTimestamp(Time.Timestamp.assertFromString("1969-07-20T20:17:00Z")),
|
||||
ValueTimestamp(Time.Timestamp.assertFromString("1969-07-20T20:17:00Z")),
|
||||
),
|
||||
(
|
||||
TDate,
|
||||
ValueDate(Time.Date.assertFromString("1879-03-14")),
|
||||
ValueDate(Time.Date.assertFromString("1879-03-14")),
|
||||
),
|
||||
(TText, ValueText("daml"), ValueText("daml")),
|
||||
(
|
||||
TNumeric(TNat(Decimal.scale)),
|
||||
ValueNumeric(Numeric.assertFromString("10.")),
|
||||
ValueNumeric(Numeric.assertFromString("10.0000000000")),
|
||||
),
|
||||
(
|
||||
TParty,
|
||||
ValueParty(Ref.Party.assertFromString("Alice")),
|
||||
ValueParty(Ref.Party.assertFromString("Alice")),
|
||||
),
|
||||
(
|
||||
TContractId(TTyCon(recordCon)),
|
||||
ValueContractId(ContractId.assertFromString("#contractId")),
|
||||
ValueContractId(ContractId.assertFromString("#contractId")),
|
||||
),
|
||||
(
|
||||
TList(TText),
|
||||
ValueList(FrontStack(ValueText("a"), ValueText("b"))),
|
||||
ValueList(FrontStack(ValueText("a"), ValueText("b"))),
|
||||
),
|
||||
(
|
||||
TTextMap(TBool),
|
||||
ValueTextMap(SortedLookupList(Map("0" -> ValueTrue, "1" -> ValueFalse))),
|
||||
ValueTextMap(SortedLookupList(Map("0" -> ValueTrue, "1" -> ValueFalse))),
|
||||
),
|
||||
(
|
||||
TOptional(TText),
|
||||
ValueOptional(Some(ValueText("text"))),
|
||||
ValueOptional(Some(ValueText("text"))),
|
||||
),
|
||||
(
|
||||
TTyCon(recordCon),
|
||||
ValueRecord(None, ImmArray(None -> ValueInt64(33))),
|
||||
ValueRecord(Some(recordCon), ImmArray(Some[Ref.Name]("field") -> ValueInt64(33))),
|
||||
),
|
||||
(
|
||||
TTyCon(variantCon),
|
||||
ValueVariant(None, "variant1", ValueText("some test")),
|
||||
ValueVariant(Some(variantCon), "variant1", ValueText("some test")),
|
||||
),
|
||||
(TTyCon(enumCon), ValueEnum(None, "value1"), ValueEnum(Some(enumCon), "value1")),
|
||||
)
|
||||
|
||||
"enrich values as expected" in {
|
||||
forAll(testCases) { (typ, input, output) =>
|
||||
enricher.enrichValue(typ, input) shouldBe ResultDone(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"enrichTransaction" should {
|
||||
|
||||
val alice = Ref.Party.assertFromString("Alice")
|
||||
|
||||
def buildTransaction(
|
||||
contract: Value[ContractId],
|
||||
key: Value[ContractId],
|
||||
record: Value[ContractId],
|
||||
) = {
|
||||
val builder = TransactionBuilder(TransactionVersion.minTypeErasure)
|
||||
val create =
|
||||
builder.create(
|
||||
id = "#01",
|
||||
template = s"$pkgId:Mod:Contract",
|
||||
argument = contract,
|
||||
signatories = Seq(alice),
|
||||
observers = Seq(alice),
|
||||
key = Some(key),
|
||||
)
|
||||
builder.add(create)
|
||||
builder.add(builder.fetch(create))
|
||||
builder.lookupByKey(create, true)
|
||||
builder.exercise(
|
||||
create,
|
||||
"Noop",
|
||||
false,
|
||||
Set(alice),
|
||||
record,
|
||||
Some(record),
|
||||
)
|
||||
builder.buildCommitted()
|
||||
}
|
||||
|
||||
"enrich transaction as expected" in {
|
||||
|
||||
val inputKey = ValueRecord(
|
||||
None,
|
||||
ImmArray(
|
||||
None -> ValueParty(alice),
|
||||
None -> Value.ValueInt64(0),
|
||||
),
|
||||
)
|
||||
|
||||
val inputContract =
|
||||
ValueRecord(
|
||||
None,
|
||||
ImmArray(
|
||||
None -> inputKey,
|
||||
None -> Value.ValueNil,
|
||||
),
|
||||
)
|
||||
|
||||
val inputRecord =
|
||||
ValueRecord(None, ImmArray(None -> ValueInt64(33)))
|
||||
|
||||
val inputTransaction = buildTransaction(
|
||||
inputContract,
|
||||
inputKey,
|
||||
inputRecord,
|
||||
)
|
||||
|
||||
val outputKey = ValueRecord(
|
||||
Some(keyCon),
|
||||
ImmArray(
|
||||
Some[Ref.Name]("party") -> ValueParty(alice),
|
||||
Some[Ref.Name]("idx") -> Value.ValueInt64(0),
|
||||
),
|
||||
)
|
||||
|
||||
val outputContract =
|
||||
ValueRecord(
|
||||
Some(contractCon),
|
||||
ImmArray(
|
||||
Some[Ref.Name]("key") -> outputKey,
|
||||
Some[Ref.Name]("cids") -> Value.ValueNil,
|
||||
),
|
||||
)
|
||||
|
||||
val outputRecord =
|
||||
ValueRecord(Some(recordCon), ImmArray(Some[Ref.Name]("field") -> ValueInt64(33)))
|
||||
|
||||
val outputTransaction = buildTransaction(
|
||||
outputContract,
|
||||
outputKey,
|
||||
outputRecord,
|
||||
)
|
||||
|
||||
enricher.enrichTransaction(inputTransaction) shouldNot be(ResultDone(inputTransaction))
|
||||
enricher.enrichTransaction(inputTransaction) shouldBe ResultDone(outputTransaction)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -90,7 +90,7 @@ final class TransactionBuilder(
|
||||
argument: Value,
|
||||
signatories: Seq[String],
|
||||
observers: Seq[String],
|
||||
key: Option[String],
|
||||
key: Option[Value],
|
||||
): Create = {
|
||||
val templateId = Ref.Identifier.assertFromString(template)
|
||||
Create(
|
||||
@ -114,6 +114,7 @@ final class TransactionBuilder(
|
||||
consuming: Boolean,
|
||||
actingParties: Set[String],
|
||||
argument: Value,
|
||||
result: Option[Value] = None,
|
||||
choiceObservers: Set[String] = Set.empty,
|
||||
byKey: Boolean = true,
|
||||
): Exercise =
|
||||
@ -129,7 +130,7 @@ final class TransactionBuilder(
|
||||
stakeholders = contract.stakeholders,
|
||||
signatories = contract.signatories,
|
||||
children = ImmArray.empty,
|
||||
exerciseResult = None,
|
||||
exerciseResult = result,
|
||||
key = contract.key,
|
||||
byKey = byKey,
|
||||
version = pkgTxVersion(contract.coinst.template.packageId),
|
||||
@ -213,12 +214,9 @@ object TransactionBuilder {
|
||||
),
|
||||
)
|
||||
|
||||
def tuple(values: String*): Value =
|
||||
record(values.zipWithIndex.map { case (v, i) => s"_$i" -> v }: _*)
|
||||
|
||||
def keyWithMaintainers(maintainers: Seq[String], value: String): KeyWithMaintainers =
|
||||
def keyWithMaintainers(maintainers: Seq[String], key: Value): KeyWithMaintainers =
|
||||
KeyWithMaintainers(
|
||||
key = tuple(maintainers :+ value: _*),
|
||||
key = key,
|
||||
maintainers = maintainers.map(Ref.Party.assertFromString).toSet,
|
||||
)
|
||||
|
||||
|
@ -44,6 +44,7 @@ object TransactionVersion {
|
||||
private[lf] val minGenMap = V11
|
||||
private[lf] val minChoiceObservers = V11
|
||||
private[lf] val minNodeVersion = V11
|
||||
private[lf] val minTypeErasure = VDev
|
||||
|
||||
private[lf] val assignNodeVersion: LanguageVersion => TransactionVersion = {
|
||||
import LanguageVersion._
|
||||
|
@ -11,6 +11,7 @@ import com.daml.ledger.api.domain.PartyDetails
|
||||
import com.daml.ledger.participant.state.v1.RejectionReason
|
||||
import com.daml.lf.transaction.GlobalKey
|
||||
import com.daml.lf.transaction.test.{TransactionBuilder => TxBuilder}
|
||||
import com.daml.lf.value.Value.ValueText
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
|
||||
@ -412,7 +413,7 @@ object PostCommitValidationSpec {
|
||||
argument = TxBuilder.record("field" -> "value"),
|
||||
signatories = Seq("Alice"),
|
||||
observers = Seq.empty,
|
||||
key = Some("key"),
|
||||
key = Some(ValueText("key")),
|
||||
)
|
||||
|
||||
private def genTestExercise(create: TxBuilder.Create): TxBuilder.Exercise =
|
||||
|
@ -365,6 +365,9 @@ class TransactionCommitterSpec extends AnyWordSpec with Matchers with MockitoSug
|
||||
val maintainer = "maintainer"
|
||||
val dummyValue = TransactionBuilder.record("field" -> "value")
|
||||
|
||||
def tuple(values: String*) =
|
||||
TransactionBuilder.record(values.zipWithIndex.map { case (v, i) => s"_$i" -> v }: _*)
|
||||
|
||||
def create(contractId: String, key: String = "key"): TransactionBuilder.Create =
|
||||
txBuilder.create(
|
||||
id = contractId,
|
||||
@ -372,7 +375,7 @@ class TransactionCommitterSpec extends AnyWordSpec with Matchers with MockitoSug
|
||||
argument = dummyValue,
|
||||
signatories = Seq(maintainer),
|
||||
observers = Seq.empty,
|
||||
key = Some(key),
|
||||
key = Some(tuple(maintainer, key)),
|
||||
)
|
||||
|
||||
def mkMismatch(
|
||||
|
Loading…
Reference in New Issue
Block a user