Skip to content

Commit 989fdc9

Browse files
fix JSON API multikey stream (#7244)
fix JSON API multikey stream In the current state, the JSON API only handles multiple keys _from different templates_. This makes it work for multiple keys from the same template too. Extracted from #7066 with the following changes: - Use of a mutable `HashSet` to test for keys, because perf. - Addition of a test at the JSON API level. CHANGELOG_BEGIN - [JSON API] Fix a bug where streaming multiple keys could silently ignore some of the given keys. CHANGELOG_END * apply @cocreature's patch https://gist.github.com/cocreature/d35367153a7331dc15cca4e5ea9098f0 * fix fmt
1 parent 09a1b44 commit 989fdc9

File tree

2 files changed

+89
-4
lines changed

2 files changed

+89
-4
lines changed

ledger-service/http-json/src/it/scala/http/WebsocketServiceIntegrationTest.scala

+77
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,83 @@ class WebsocketServiceIntegrationTest
450450
} yield resumes.foldLeft(1 shouldBe 1)((_, a) => a)
451451
}
452452

453+
"fetch multiple keys should work" in withHttpService { (uri, encoder, _) =>
454+
def matches(expected: Seq[JsValue], actual: Seq[JsValue]): Boolean =
455+
expected.length == actual.length && (expected, actual).zipped.forall {
456+
case (exp, act) => matchesJs(exp, act)
457+
}
458+
// matches if all the values specified in expected appear with the same
459+
// value in actual; actual is allowed to have extra fields. Arrays must
460+
// have the same length.
461+
def matchesJs(expected: spray.json.JsValue, actual: spray.json.JsValue): Boolean = {
462+
import spray.json._
463+
(expected, actual) match {
464+
case (JsArray(expected), JsArray(actual)) =>
465+
expected.length == actual.length && matches(expected, actual)
466+
case (JsObject(expected), JsObject(actual)) =>
467+
expected.keys.forall(k => matchesJs(expected(k), actual(k)))
468+
case (JsString(expected), JsString(actual)) => expected == actual
469+
case (JsNumber(expected), JsNumber(actual)) => expected == actual
470+
case (JsBoolean(expected), JsBoolean(actual)) => expected == actual
471+
case (JsNull, JsNull) => true
472+
case _ => false
473+
}
474+
}
475+
def create(account: String): Future[domain.ContractId] =
476+
for {
477+
r <- postCreateCommand(accountCreateCommand(domain.Party("Alice"), account), encoder, uri)
478+
} yield {
479+
assert(r._1.isSuccess)
480+
getContractId(getResult(r._2))
481+
}
482+
def archive(id: domain.ContractId): Future[Assertion] =
483+
for {
484+
r <- postArchiveCommand(domain.TemplateId(None, "Account", "Account"), id, encoder, uri)
485+
} yield {
486+
assert(r._1.isSuccess)
487+
}
488+
val req =
489+
"""
490+
|[{"templateId": "Account:Account", "key": ["Alice", "abc123"]},
491+
| {"templateId": "Account:Account", "key": ["Alice", "def456"]}]
492+
|""".stripMargin
493+
val futureResults =
494+
singleClientFetchStream(jwt, uri, req).via(parseResp).runWith(Sink.seq[JsValue])
495+
496+
for {
497+
cid1 <- create("abc123")
498+
_ <- create("abc124")
499+
_ <- create("abc125")
500+
cid2 <- create("def456")
501+
_ <- archive(cid2)
502+
_ <- archive(cid1)
503+
results <- futureResults
504+
} yield {
505+
val expected: Seq[JsValue] = {
506+
import spray.json._
507+
Seq(
508+
"""
509+
|{"events": []}
510+
|""".stripMargin.parseJson,
511+
"""
512+
|{"events":[{"created":{"payload":{"number":"abc123"}}}]}
513+
|""".stripMargin.parseJson,
514+
"""
515+
|{"events":[{"created":{"payload":{"number":"def456"}}}]}
516+
|""".stripMargin.parseJson,
517+
"""
518+
|{"events":[{"archived":{}}]}
519+
|""".stripMargin.parseJson,
520+
"""
521+
|{"events":[{"archived":{}}]}
522+
|""".stripMargin.parseJson
523+
)
524+
}
525+
assert(matches(expected, results))
526+
527+
}
528+
}
529+
453530
"fetch should should return an error if empty list of (templateId, key) pairs is passed" in withHttpService {
454531
(uri, _, _) =>
455532
singleClientFetchStream(jwt, uri, "[]")

ledger-service/http-json/src/main/scala/com/digitalasset/http/WebSocketService.scala

+12-4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import com.daml.http.util.FlowUtil.allowOnlyFirstInput
3333
import spray.json.{JsArray, JsObject, JsValue, JsonReader}
3434

3535
import scala.collection.compat._
36+
import scala.collection.mutable.HashSet
3637
import scala.concurrent.{ExecutionContext, Future}
3738
import scala.util.{Failure, Success}
3839

@@ -257,11 +258,18 @@ object WebSocketService {
257258
.toLeftDisjunction(x.ekey.templateId)
258259
}
259260

260-
val q: Map[domain.TemplateId.RequiredPkg, LfV] = resolvedWithKey.toMap
261+
val q: Map[domain.TemplateId.RequiredPkg, HashSet[LfV]] =
262+
resolvedWithKey.foldLeft(Map.empty[domain.TemplateId.RequiredPkg, HashSet[LfV]])(
263+
(acc, el) =>
264+
acc.get(el._1) match {
265+
case Some(v) => acc.updated(el._1, v += el._2)
266+
case None => acc.updated(el._1, HashSet(el._2))
267+
})
261268
val fn: domain.ActiveContract[LfV] => Option[Positive] = { a =>
262-
if (q.get(a.templateId).exists(k => domain.ActiveContract.matchesKey(k)(a)))
263-
Some(())
264-
else None
269+
a.key match {
270+
case None => None
271+
case Some(k) => if (q.getOrElse(a.templateId, HashSet()).contains(k)) Some(()) else None
272+
}
265273
}
266274
(q.keySet, unresolved, fn)
267275
}

0 commit comments

Comments
 (0)