Skip to content

Commit a2ae58a

Browse files
Controversial change 2: restore daml/lf/value/json
Reverts (a small) part of DACH-NY/canton#27917 Seems easier than reinvent it somehow / rewrite all the places we use it for converting things. Signed-off-by: Martin Florian <martin.florian@digitalasset.com>
1 parent 98e1026 commit a2ae58a

File tree

6 files changed

+503
-0
lines changed

6 files changed

+503
-0
lines changed

apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/AcsJdbcTypes.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.daml.ledger.javaapi.data.codegen.json.JsonLfWriter
99
import com.daml.ledger.javaapi.data.codegen.{ContractId, DamlRecord, DefinedDataType}
1010
import com.digitalasset.canton.config.CantonRequireTypes.{String2066, String300}
1111
import com.digitalasset.canton.data.Offset
12+
import com.digitalasset.canton.daml.lf.value.json.ApiCodecCompressed
1213
import com.digitalasset.canton.topology.{Member, PartyId, SynchronizerId}
1314
import com.digitalasset.daml.lf.data.Ref.HexString
1415
import com.digitalasset.daml.lf.data.Time.Timestamp

apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/Contract.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.digitalasset.daml.lf.data.Time.Timestamp
2020
import org.lfdecentralizedtrust.splice.http.v0.definitions as http
2121
import org.lfdecentralizedtrust.splice.http.v0.definitions.MaybeCachedContract
2222
import org.lfdecentralizedtrust.splice.util.JavaDecodeUtil
23+
import com.digitalasset.canton.daml.lf.value.json.ApiCodecCompressed
2324
import com.digitalasset.canton.ProtoDeserializationError
2425
import com.digitalasset.canton.ledger.api.validation.ValueValidator
2526
import com.digitalasset.canton.logging.ErrorLoggingContext
Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
// Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package com.digitalasset.canton.daml.lf.value.json
5+
6+
import com.digitalasset.canton.daml.lf.value.json.NavigatorModelAliases as Model
7+
import com.digitalasset.daml.lf.data.ImmArray.ImmArraySeq
8+
import com.digitalasset.daml.lf.data.ScalazEqual.*
9+
import com.digitalasset.daml.lf.data.{
10+
FrontStack,
11+
ImmArray,
12+
Numeric as LfNumeric,
13+
Ref,
14+
SortedLookupList,
15+
Time,
16+
}
17+
import com.digitalasset.daml.lf.typesig
18+
import com.digitalasset.daml.lf.value.Value as V
19+
import com.digitalasset.daml.lf.value.Value.ContractId
20+
import scalaz.syntax.std.string.*
21+
import scalaz.{@@, Tag}
22+
import spray.json.*
23+
24+
import java.time.Instant
25+
import scala.annotation.nowarn
26+
import scala.util.Try
27+
28+
import NavigatorModelAliases.DamlLfIdentifier
29+
import ApiValueImplicits.*
30+
31+
/** A compressed encoding of API values.
32+
*
33+
* The encoded values do not include type information. For example, it is impossible to distinguish
34+
* party and text values in the encoded format.
35+
*
36+
* Therefore, this JSON format can only decode given a target type.
37+
*
38+
* `apiValueJsonReader` can create a JSON reader with the necessary type information.
39+
*
40+
* @param encodeDecimalAsString
41+
* Not used yet.
42+
* @param encodeInt64AsString
43+
* Not used yet.
44+
*/
45+
class ApiCodecCompressed(val encodeDecimalAsString: Boolean, val encodeInt64AsString: Boolean)(
46+
implicit
47+
readCid: JsonReader[ContractId],
48+
writeCid: JsonWriter[ContractId],
49+
) { self =>
50+
51+
// ------------------------------------------------------------------------------------------------------------------
52+
// Encoding
53+
// ------------------------------------------------------------------------------------------------------------------
54+
def apiValueToJsValue(value: V): JsValue = value match {
55+
case v: V.ValueRecord => apiRecordToJsValue(v)
56+
case v: V.ValueVariant => apiVariantToJsValue(v)
57+
case v: V.ValueEnum => apiEnumToJsValue(v)
58+
case v: V.ValueList => apiListToJsValue(v)
59+
case V.ValueText(v) => JsString(v)
60+
case V.ValueInt64(v) => if (encodeInt64AsString) JsString((v: Long).toString) else JsNumber(v)
61+
case V.ValueNumeric(v) =>
62+
if (encodeDecimalAsString) JsString(LfNumeric.toUnscaledString(v)) else JsNumber(v)
63+
case V.ValueBool(v) => JsBoolean(v)
64+
case V.ValueContractId(v) => apiContractIdToJsValue(v)
65+
case t: V.ValueTimestamp => JsString(t.toIso8601)
66+
case d: V.ValueDate => JsString(d.toIso8601)
67+
case V.ValueParty(v) => JsString(v)
68+
case V.ValueUnit => JsObject.empty
69+
case V.ValueOptional(None) => JsNull
70+
case V.ValueOptional(Some(v)) =>
71+
v match {
72+
case V.ValueOptional(None) => JsArray()
73+
case V.ValueOptional(Some(_)) => JsArray(apiValueToJsValue(v))
74+
case _ => apiValueToJsValue(v)
75+
}
76+
case textMap: V.ValueTextMap =>
77+
apiMapToJsValue(textMap)
78+
case genMap: V.ValueGenMap =>
79+
apiGenMapToJsValue(genMap)
80+
}
81+
82+
@throws[SerializationException]
83+
private[this] final def apiContractIdToJsValue(v: ContractId): JsValue = v.toJson
84+
85+
private[this] def apiListToJsValue(value: V.ValueList): JsValue =
86+
JsArray(value.values.map(apiValueToJsValue(_)).toImmArray.toSeq*)
87+
88+
private[this] def apiVariantToJsValue(value: V.ValueVariant): JsValue =
89+
JsonVariant(value.variant, apiValueToJsValue(value.value))
90+
91+
private[this] def apiEnumToJsValue(value: V.ValueEnum): JsValue =
92+
JsString(value.value)
93+
94+
private[ApiCodecCompressed] def apiRecordToJsValue(value: V.ValueRecord): JsValue = {
95+
val namedOrNoneFields = value.fields.toSeq collect {
96+
case (Some(k), v) => Some((k, v))
97+
case (_, V.ValueOptional(None)) => None
98+
}
99+
if (namedOrNoneFields.lengthIs == value.fields.length)
100+
JsObject(namedOrNoneFields.iterator.collect { case Some((flabel, fvalue)) =>
101+
(flabel: String) -> apiValueToJsValue(fvalue)
102+
}.toMap)
103+
else
104+
JsArray(value.fields.toSeq.map { case (_, fvalue) =>
105+
apiValueToJsValue(fvalue)
106+
}*)
107+
}
108+
109+
private[this] def apiMapToJsValue(value: V.ValueTextMap): JsValue =
110+
JsObject(
111+
value.value
112+
.mapValue(apiValueToJsValue)
113+
.toHashMap
114+
)
115+
116+
private[this] def apiGenMapToJsValue(value: V.ValueGenMap): JsValue =
117+
JsArray(
118+
value.entries.map { case (key, value) =>
119+
JsArray(apiValueToJsValue(key), apiValueToJsValue(value))
120+
}.toSeq*
121+
)
122+
123+
// ------------------------------------------------------------------------------------------------------------------
124+
// Decoding - this needs access to Daml-LF types
125+
// ------------------------------------------------------------------------------------------------------------------
126+
127+
@throws[DeserializationException]
128+
private[this] final def jsValueToApiContractId(value: JsValue): ContractId =
129+
value.convertTo[ContractId]
130+
131+
@SuppressWarnings(Array("org.wartremover.warts.IterableOps"))
132+
private[this] def jsValueToApiPrimitive(
133+
value: JsValue,
134+
prim: Model.DamlLfTypePrim,
135+
defs: Model.DamlLfTypeLookup,
136+
): V =
137+
(prim.typ, value).match2 {
138+
case Model.DamlLfPrimType.Int64 => {
139+
case JsString(v) => V.ValueInt64(assertDE(v.parseLong.leftMap(_.getMessage).toEither))
140+
case JsNumber(v) if v.isValidLong => V.ValueInt64(v.toLongExact)
141+
}
142+
case Model.DamlLfPrimType.Text => { case JsString(v) => V.ValueText(v) }
143+
case Model.DamlLfPrimType.Party => { case JsString(v) =>
144+
V.ValueParty(assertDE(Ref.Party fromString v))
145+
}
146+
case Model.DamlLfPrimType.ContractId => { case v =>
147+
V.ValueContractId(jsValueToApiContractId(v))
148+
}
149+
case Model.DamlLfPrimType.Unit => { case JsObject(_) => V.ValueUnit }
150+
case Model.DamlLfPrimType.Timestamp => { case JsString(v) =>
151+
val optTimestamp = for {
152+
instant <- Try(Instant.parse(v)).toEither.left.map(_.getMessage)
153+
timestamp <- Time.Timestamp.fromInstant(instant)
154+
} yield timestamp
155+
V.ValueTimestamp(assertDE(optTimestamp))
156+
}
157+
case Model.DamlLfPrimType.Date => { case JsString(v) =>
158+
try {
159+
V.ValueDate.fromIso8601(v)
160+
} catch {
161+
case _: java.time.format.DateTimeParseException | _: IllegalArgumentException =>
162+
throw DeserializationException(s"Invalid date: $v")
163+
}
164+
}
165+
case Model.DamlLfPrimType.Bool => { case JsBoolean(v) => V.ValueBool(v) }
166+
case Model.DamlLfPrimType.List => { case JsArray(v) =>
167+
V.ValueList(
168+
v.iterator.map(e => jsValueToApiValue(e, prim.typArgs.head, defs)).to(FrontStack)
169+
)
170+
}
171+
case Model.DamlLfPrimType.Optional =>
172+
val typArg = prim.typArgs.head
173+
val useArray = nestsOptional(prim)
174+
175+
{
176+
case JsNull => V.ValueNone
177+
case JsArray(ov) if useArray =>
178+
ov match {
179+
case Seq() => V.ValueOptional(Some(V.ValueNone))
180+
case Seq(v) => V.ValueOptional(Some(jsValueToApiValue(v, typArg, defs)))
181+
case _ =>
182+
deserializationError(s"Can't read ${value.prettyPrint} as Optional of Optional")
183+
}
184+
case _ if !useArray => V.ValueOptional(Some(jsValueToApiValue(value, typArg, defs)))
185+
}
186+
case Model.DamlLfPrimType.TextMap => { case JsObject(a) =>
187+
V.ValueTextMap(SortedLookupList(a.transform { (_, v) =>
188+
jsValueToApiValue(v, prim.typArgs.head, defs)
189+
}))
190+
}
191+
case Model.DamlLfPrimType.GenMap =>
192+
val Seq(kType, vType) = prim.typArgs: @nowarn("msg=match may not be exhaustive")
193+
194+
{ case JsArray(entries) =>
195+
type OK[K] = Vector[(K, V)]
196+
val decEntries: Vector[(V @@ defs.type, V)] = Tag
197+
.subst[V, OK, defs.type](entries.map {
198+
case JsArray(Vector(key, value)) =>
199+
jsValueToApiValue(key, kType, defs) ->
200+
jsValueToApiValue(value, vType, defs)
201+
case _ =>
202+
deserializationError(s"Can't read ${value.prettyPrint} as key+value of $prim")
203+
})
204+
V.ValueGenMap(Tag.unsubst[V, OK, defs.type](decEntries).to(ImmArray))
205+
}
206+
207+
}(fallback = deserializationError(s"Can't read ${value.prettyPrint} as $prim"))
208+
209+
private[this] def nestsOptional(prim: typesig.TypePrim): Boolean =
210+
prim match {
211+
case typesig.TypePrim(_, Seq(typesig.TypePrim(typesig.PrimType.Optional, _))) => true
212+
case _ => false
213+
}
214+
215+
private[this] def jsValueToApiDataType(
216+
value: JsValue,
217+
id: DamlLfIdentifier,
218+
dt: Model.DamlLfDataType,
219+
defs: Model.DamlLfTypeLookup,
220+
): V =
221+
(dt, value).match2 {
222+
case Model.DamlLfRecord(fields) => {
223+
case JsObject(v) =>
224+
V.ValueRecord(
225+
Some(id),
226+
fields.map { case (fName, fTy) =>
227+
val fValue = v
228+
.get(fName)
229+
.map(jsValueToApiValue(_, fTy, defs))
230+
.getOrElse(fTy match {
231+
case typesig.TypePrim(typesig.PrimType.Optional, _) => V.ValueNone
232+
case _ =>
233+
deserializationError(
234+
s"Can't read ${value.prettyPrint} as DamlLfRecord $id, missing field '$fName'"
235+
)
236+
})
237+
(Some(fName), fValue)
238+
}.toImmArray,
239+
)
240+
case JsArray(fValues) =>
241+
if (fValues.sizeCompare(fields) != 0)
242+
deserializationError(
243+
s"Can't read ${value.prettyPrint} as DamlLfRecord $id, wrong number of record fields (expected ${fields.length}, found ${fValues.length})."
244+
)
245+
else
246+
V.ValueRecord(
247+
Some(id),
248+
(fields zip fValues).map { case ((fName, fTy), fValue) =>
249+
(Some(fName), jsValueToApiValue(fValue, fTy, defs))
250+
}.toImmArray,
251+
)
252+
}
253+
case Model.DamlLfVariant(cons) => {
254+
case JsonVariant(tag, nestedValue) =>
255+
val (constructorName, constructorType) = cons.toList
256+
.find(_._1 == tag)
257+
.getOrElse(
258+
deserializationError(
259+
s"Can't read ${value.compactPrint} as DamlLfVariant $id, unknown constructor $tag"
260+
)
261+
)
262+
263+
V.ValueVariant(
264+
Some(id),
265+
constructorName,
266+
jsValueToApiValue(nestedValue, constructorType, defs),
267+
)
268+
case _ =>
269+
deserializationError(
270+
s"Can't read ${value.prettyPrint} as DamlLfVariant $id, expected JsObject with 'tag' and 'value' fields"
271+
)
272+
}
273+
case Model.DamlLfEnum(cons) => { case JsString(c) =>
274+
cons
275+
.collectFirst { case kc if kc == c => kc }
276+
.map(
277+
V.ValueEnum(
278+
Some(id),
279+
_,
280+
)
281+
)
282+
.getOrElse(
283+
deserializationError(
284+
s"Can't read ${value.prettyPrint} as DamlLfEnum $id, unknown constructor $c"
285+
)
286+
)
287+
}
288+
}(fallback = deserializationError(s"Can't read ${value.prettyPrint} as $dt"))
289+
290+
/** Deserialize a value, given the type */
291+
def jsValueToApiValue(
292+
value: JsValue,
293+
typ: Model.DamlLfType,
294+
defs: Model.DamlLfTypeLookup,
295+
): V =
296+
typ match {
297+
case prim: Model.DamlLfTypePrim =>
298+
jsValueToApiPrimitive(value, prim, defs)
299+
case typeCon: Model.DamlLfTypeCon =>
300+
val id = typeCon.tycon.identifier
301+
val dt =
302+
typeCon.instantiate(defs(id).getOrElse(deserializationError(s"Type $id not found")))
303+
jsValueToApiDataType(value, id, dt, defs)
304+
case Model.DamlLfTypeNumeric(scale) =>
305+
val numericOrError = value match {
306+
case JsString(v) =>
307+
LfNumeric.checkWithinBoundsAndRound(scale, BigDecimal(v))
308+
case JsNumber(v) =>
309+
LfNumeric.checkWithinBoundsAndRound(scale, v)
310+
case _ =>
311+
deserializationError(s"Can't read ${value.prettyPrint} as (Numeric $scale)")
312+
}
313+
V.ValueNumeric(assertDE(numericOrError))
314+
case Model.DamlLfTypeVar(_) =>
315+
deserializationError(s"Can't read ${value.prettyPrint} as DamlLfTypeVar")
316+
}
317+
318+
/** Deserialize a value, given the ID of the corresponding closed type */
319+
def jsValueToApiValue(
320+
value: JsValue,
321+
id: Model.DamlLfIdentifier,
322+
defs: Model.DamlLfTypeLookup,
323+
): V = {
324+
val typeCon = Model.DamlLfTypeCon(Model.DamlLfTypeConId(id), ImmArraySeq())
325+
val dt = typeCon.instantiate(defs(id).getOrElse(deserializationError(s"Type $id not found")))
326+
jsValueToApiDataType(value, id, dt, defs)
327+
}
328+
329+
private[this] def assertDE[A](ea: Either[String, A]): A =
330+
ea.fold(deserializationError(_), identity)
331+
332+
private[json] def copy(
333+
encodeDecimalAsString: Boolean = this.encodeDecimalAsString,
334+
encodeInt64AsString: Boolean = this.encodeInt64AsString,
335+
): ApiCodecCompressed =
336+
new ApiCodecCompressed(
337+
encodeDecimalAsString = encodeDecimalAsString,
338+
encodeInt64AsString = encodeInt64AsString,
339+
)
340+
}
341+
342+
private[json] object JsonContractIdFormat {
343+
implicit val ContractIdFormat: JsonFormat[ContractId] =
344+
new JsonFormat[ContractId] {
345+
override def write(obj: ContractId) =
346+
JsString(obj.coid)
347+
override def read(json: JsValue) = json match {
348+
case JsString(s) =>
349+
ContractId.fromString(s).fold(deserializationError(_), identity)
350+
case _ => deserializationError("ContractId must be a string")
351+
}
352+
}
353+
}
354+
import JsonContractIdFormat.*
355+
356+
object ApiCodecCompressed
357+
extends ApiCodecCompressed(encodeDecimalAsString = true, encodeInt64AsString = true) {
358+
// ------------------------------------------------------------------------------------------------------------------
359+
// Implicits that can be imported to write JSON
360+
// ------------------------------------------------------------------------------------------------------------------
361+
object JsonImplicits extends DefaultJsonProtocol {
362+
implicit object ApiValueJsonFormat extends RootJsonWriter[Model.ApiValue] {
363+
def write(v: Model.ApiValue): JsValue = apiValueToJsValue(v)
364+
}
365+
implicit object ApiRecordJsonFormat extends RootJsonWriter[Model.ApiRecord] {
366+
def write(v: Model.ApiRecord): JsValue = apiRecordToJsValue(v)
367+
}
368+
369+
implicit val ContractIdFormat: JsonFormat[ContractId] = JsonContractIdFormat.ContractIdFormat
370+
}
371+
}

0 commit comments

Comments
 (0)