mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 09:17:43 +03:00
Extend /v1/parties endpoint to support fetch by party IDs (#4680)
* Extend /party endpoint to allow specifying party ids * Extend /party endpoint to allow specifying party ids * Update docs CHANGELOG_BEGIN [JSON API - Experimental] Fetch Parties by their Identifiers. See #4512 ``/v1/parties`` endpoint supports POST method now, which expects a JSON array of party identifiers as an input. CHANGELOG_END * minor update * minor update * Use type alias * Add warnings to the sync response * test cases * update docs, add test case for an empty input * cleanup * cleanup * Addressing code review comments
This commit is contained in:
parent
8c14d16718
commit
d58bb4597e
@ -803,12 +803,28 @@ Nonempty HTTP Response with Unknown Template IDs Warning
|
||||
"status": 200
|
||||
}
|
||||
|
||||
Fetch All Known Parties
|
||||
***********************
|
||||
Fetch Parties by Identifiers
|
||||
****************************
|
||||
|
||||
- URL: ``/v1/parties``
|
||||
- Method: ``GET``
|
||||
- Content: <EMPTY>
|
||||
- Method: ``POST``
|
||||
- Content-Type: ``application/json``
|
||||
- Content:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
["Alice", "Bob", "Dave"]
|
||||
|
||||
If empty JSON array is passed: ``[]``, this endpoint returns BadRequest(400) error:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"status": 400,
|
||||
"errors": [
|
||||
"JsonReaderError. Cannot read JSON: <[]>. Cause: spray.json.DeserializationException: must be a list with at least 1 element"
|
||||
]
|
||||
}
|
||||
|
||||
HTTP Response
|
||||
=============
|
||||
@ -819,15 +835,69 @@ HTTP Response
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"status": 200,
|
||||
"result": [
|
||||
{
|
||||
"party": "Alice",
|
||||
"isLocal": true
|
||||
}
|
||||
]
|
||||
"status": 200,
|
||||
"result": [
|
||||
{
|
||||
"identifier": "Alice",
|
||||
"displayName": "Alice & Co. LLC",
|
||||
"isLocal": true
|
||||
},
|
||||
{
|
||||
"identifier": "Bob",
|
||||
"displayName": "Bob & Co. LLC",
|
||||
"isLocal": true
|
||||
},
|
||||
{
|
||||
"identifier": "Dave",
|
||||
"isLocal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Please note that the order of the party objects in the response is not guaranteed to match the order of the passed party identifiers.
|
||||
|
||||
Where
|
||||
|
||||
- ``identifier`` -- a stable unique identifier of a DAML party,
|
||||
- ``displayName`` -- optional human readable name associated with the party. Might not be unique,
|
||||
- ``isLocal`` -- true if party is hosted by the backing participant.
|
||||
|
||||
HTTP Response with Unknown Parties Warning
|
||||
============================================
|
||||
|
||||
- Content-Type: ``application/json``
|
||||
- Content:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"result": [
|
||||
{
|
||||
"identifier": "Alice",
|
||||
"displayName": "Alice & Co. LLC",
|
||||
"isLocal": true
|
||||
}
|
||||
],
|
||||
"warnings": {
|
||||
"unknownParties": [
|
||||
"Erin"
|
||||
]
|
||||
},
|
||||
"status": 200
|
||||
}
|
||||
|
||||
Fetch All Known Parties
|
||||
***********************
|
||||
|
||||
- URL: ``/v1/parties``
|
||||
- Method: ``GET``
|
||||
- Content: <EMPTY>
|
||||
|
||||
HTTP Response
|
||||
=============
|
||||
|
||||
The response is the same as for the POST method above.
|
||||
|
||||
Streaming API
|
||||
*************
|
||||
|
||||
|
@ -15,6 +15,7 @@ import com.digitalasset.http.EndpointsCompanion._
|
||||
import com.digitalasset.http.Statement.discard
|
||||
import com.digitalasset.http.domain.JwtPayload
|
||||
import com.digitalasset.http.json._
|
||||
import com.digitalasset.http.util.Collections.toNonEmptySet
|
||||
import com.digitalasset.http.util.FutureUtil.{either, eitherT, rightT}
|
||||
import com.digitalasset.http.util.{ApiValueToLfValueConverter, FutureUtil}
|
||||
import com.digitalasset.jwt.domain.Jwt
|
||||
@ -23,14 +24,16 @@ import com.digitalasset.ledger.api.{v1 => lav1}
|
||||
import com.digitalasset.util.ExceptionOps._
|
||||
import com.typesafe.scalalogging.StrictLogging
|
||||
import scalaz.std.scalaFuture._
|
||||
import scalaz.syntax.bitraverse._
|
||||
import scalaz.syntax.show._
|
||||
import scalaz.syntax.std.option._
|
||||
import scalaz.syntax.traverse._
|
||||
import scalaz.{-\/, EitherT, Show, \/, \/-}
|
||||
import scalaz.{-\/, Bitraverse, EitherT, NonEmptyList, Show, \/, \/-}
|
||||
import spray.json._
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.language.higherKinds
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
@SuppressWarnings(Array("org.wartremover.warts.Any"))
|
||||
@ -56,10 +59,11 @@ class Endpoints(
|
||||
case req @ HttpRequest(POST, Uri.Path("/v1/fetch"), _, _, _) => httpResponse(fetch(req))
|
||||
case req @ HttpRequest(GET, Uri.Path("/v1/query"), _, _, _) => httpResponse(retrieveAll(req))
|
||||
case req @ HttpRequest(POST, Uri.Path("/v1/query"), _, _, _) => httpResponse(query(req))
|
||||
case req @ HttpRequest(GET, Uri.Path("/v1/parties"), _, _, _) => httpResponse(parties(req))
|
||||
case req @ HttpRequest(GET, Uri.Path("/v1/parties"), _, _, _) => httpResponse(allParties(req))
|
||||
case req @ HttpRequest(POST, Uri.Path("/v1/parties"), _, _, _) => httpResponse(parties(req))
|
||||
}
|
||||
|
||||
def create(req: HttpRequest): ET[JsValue] =
|
||||
def create(req: HttpRequest): ET[domain.OkResponse[JsValue, Unit]] =
|
||||
for {
|
||||
t3 <- FutureUtil.eitherT(input(req)): ET[(Jwt, JwtPayload, String)]
|
||||
|
||||
@ -77,9 +81,9 @@ class Endpoints(
|
||||
|
||||
jsVal <- either(encoder.encodeV(ac).leftMap(e => ServerError(e.shows))): ET[JsValue]
|
||||
|
||||
} yield jsVal
|
||||
} yield domain.OkResponse(jsVal)
|
||||
|
||||
def exercise(req: HttpRequest): ET[JsValue] =
|
||||
def exercise(req: HttpRequest): ET[domain.OkResponse[JsValue, Unit]] =
|
||||
for {
|
||||
t3 <- eitherT(input(req)): ET[(Jwt, JwtPayload, String)]
|
||||
|
||||
@ -107,11 +111,11 @@ class Endpoints(
|
||||
|
||||
jsResp <- either(lfResp.traverse(lfValueToJsValue)): ET[domain.ExerciseResponse[JsValue]]
|
||||
|
||||
jsVal <- either(SprayJson.encode(jsResp).leftMap(e => ServerError(e.shows))): ET[JsValue]
|
||||
jsVal <- either(toJsValue(jsResp)): ET[JsValue]
|
||||
|
||||
} yield jsVal
|
||||
} yield domain.OkResponse(jsVal)
|
||||
|
||||
def fetch(req: HttpRequest): ET[JsValue] =
|
||||
def fetch(req: HttpRequest): ET[domain.OkResponse[JsValue, Unit]] =
|
||||
for {
|
||||
input <- FutureUtil.eitherT(input(req)): ET[(Jwt, JwtPayload, String)]
|
||||
|
||||
@ -135,7 +139,7 @@ class Endpoints(
|
||||
ac.cata(x => lfAcToJsValue(x).leftMap(e => ServerError(e.shows)), \/-(JsNull))
|
||||
): ET[JsValue]
|
||||
|
||||
} yield jsVal
|
||||
} yield domain.OkResponse(jsVal)
|
||||
|
||||
def retrieveAll(req: HttpRequest): Future[Error \/ SearchResult[Error \/ JsValue]] =
|
||||
input(req).map {
|
||||
@ -167,19 +171,45 @@ class Endpoints(
|
||||
|
||||
val jsValSource: Source[Error \/ JsValue, NotUsed] = result.source
|
||||
.via(handleSourceFailure)
|
||||
.map(_.flatMap(jsAcToJsValue))
|
||||
.map(_.flatMap(toJsValue[domain.ActiveContract[JsValue]](_)))
|
||||
|
||||
result.copy(source = jsValSource): SearchResult[Error \/ JsValue]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def parties(req: HttpRequest): ET[JsValue] =
|
||||
def allParties(req: HttpRequest): ET[domain.OkResponse[JsValue, JsValue]] =
|
||||
for {
|
||||
_ <- FutureUtil.eitherT(input(req)): ET[(Jwt, JwtPayload, String)]
|
||||
ps <- rightT(partiesService.allParties()): ET[List[domain.PartyDetails]]
|
||||
jsVal <- either(SprayJson.encode(ps)).leftMap(e => ServerError(e.shows)): ET[JsValue]
|
||||
} yield jsVal
|
||||
t3 <- eitherT(input(req)): ET[(Jwt, JwtPayload, String)]
|
||||
ps <- rightT(partiesService.allParties(t3._1)): ET[List[domain.PartyDetails]]
|
||||
resp = domain.OkResponse(ps, Option.empty[Unit])
|
||||
result <- either(resp.bitraverse(toJsValue(_), toJsValue(_)))
|
||||
} yield result
|
||||
|
||||
def parties(req: HttpRequest): ET[domain.OkResponse[JsValue, JsValue]] =
|
||||
for {
|
||||
t3 <- eitherT(input(req)): ET[(Jwt, JwtPayload, String)]
|
||||
|
||||
(jwt, _, reqBody) = t3
|
||||
|
||||
cmd <- either(
|
||||
SprayJson
|
||||
.decode[NonEmptyList[domain.Party]](reqBody)
|
||||
.leftMap(e => InvalidUserInput(e.shows))
|
||||
): ET[NonEmptyList[domain.Party]]
|
||||
|
||||
ps <- eitherT(
|
||||
handleFutureFailure(
|
||||
partiesService.parties(jwt, toNonEmptySet(cmd))
|
||||
)): ET[(Set[domain.PartyDetails], Set[domain.Party])]
|
||||
|
||||
resp: domain.OkResponse[List[domain.PartyDetails], domain.UnknownParties] = okResponse(
|
||||
ps._1.toList,
|
||||
ps._2.toList)
|
||||
|
||||
result <- either(resp.bitraverse(toJsValue(_), toJsValue(_)))
|
||||
|
||||
} yield result
|
||||
|
||||
private def handleFutureFailure[A: Show, B](fa: Future[A \/ B]): Future[ServerError \/ B] =
|
||||
fa.map(a => a.leftMap(e => ServerError(e.shows))).recover {
|
||||
@ -204,17 +234,6 @@ class Endpoints(
|
||||
-\/(ServerError(e.description))
|
||||
}
|
||||
|
||||
private def httpResponse(output: ET[JsValue]): Future[HttpResponse] = {
|
||||
val fa: Future[Error \/ JsValue] = output.run
|
||||
fa.map {
|
||||
case \/-(a) => httpResponseOk(a)
|
||||
case -\/(e) => httpResponseError(e)
|
||||
}
|
||||
.recover {
|
||||
case NonFatal(e) => httpResponseError(ServerError(e.description))
|
||||
}
|
||||
}
|
||||
|
||||
private def httpResponse(
|
||||
output: Future[Error \/ SearchResult[Error \/ JsValue]]): Future[HttpResponse] =
|
||||
output
|
||||
@ -245,6 +264,21 @@ class Endpoints(
|
||||
)
|
||||
}
|
||||
|
||||
private def httpResponse[A: JsonWriter, B: JsonWriter](
|
||||
result: ET[domain.OkResponse[A, B]]
|
||||
): Future[HttpResponse] = {
|
||||
val fa: Future[Error \/ JsValue] = result.flatMap(x => either(toJsValueWithBitraverse(x))).run
|
||||
fa.map {
|
||||
case -\/(e) =>
|
||||
httpResponseError(e)
|
||||
case \/-(r) =>
|
||||
HttpResponse(entity = HttpEntity.Strict(ContentTypes.`application/json`, format(r)))
|
||||
}
|
||||
.recover {
|
||||
case NonFatal(e) => httpResponseError(ServerError(e.description))
|
||||
}
|
||||
}
|
||||
|
||||
private[http] def input(req: HttpRequest): Future[Unauthorized \/ (Jwt, JwtPayload, String)] = {
|
||||
findJwt(req).flatMap(decodeAndParsePayload(_, decodeJwt)) match {
|
||||
case e @ -\/(_) =>
|
||||
@ -304,10 +338,30 @@ object Endpoints {
|
||||
private def lfAcToJsValue(a: domain.ActiveContract[LfValue]): Error \/ JsValue = {
|
||||
for {
|
||||
b <- a.traverse(lfValueToJsValue): Error \/ domain.ActiveContract[JsValue]
|
||||
c <- SprayJson.encode(b).leftMap(e => ServerError(e.shows))
|
||||
c <- toJsValue(b)
|
||||
} yield c
|
||||
}
|
||||
|
||||
private def jsAcToJsValue(a: domain.ActiveContract[JsValue]): Error \/ JsValue =
|
||||
private def okResponse(parties: List[domain.PartyDetails], unknownParties: List[domain.Party])
|
||||
: domain.OkResponse[List[domain.PartyDetails], domain.UnknownParties] = {
|
||||
if (unknownParties.isEmpty) domain.OkResponse(parties, Option.empty[domain.UnknownParties])
|
||||
else domain.OkResponse(parties, Some(domain.UnknownParties(unknownParties)))
|
||||
}
|
||||
|
||||
private def toJsValue[A: JsonWriter](a: A): Error \/ JsValue =
|
||||
SprayJson.encode(a).leftMap(e => ServerError(e.shows))
|
||||
|
||||
@SuppressWarnings(Array("org.wartremover.warts.Any"))
|
||||
private def toJsValueWithBitraverse[F[_, _], A, B](fab: F[A, B])(
|
||||
implicit ev1: Bitraverse[F],
|
||||
ev2: JsonWriter[F[JsValue, JsValue]],
|
||||
ev3: JsonWriter[A],
|
||||
ev4: JsonWriter[B]): Error \/ JsValue =
|
||||
for {
|
||||
fjj <- fab.bitraverse(
|
||||
a => toJsValue(a),
|
||||
b => toJsValue(b)
|
||||
): Error \/ F[JsValue, JsValue]
|
||||
jsVal <- toJsValue(fjj)
|
||||
} yield jsVal
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ import com.digitalasset.http.json.{
|
||||
ApiValueToJsValueConverter,
|
||||
DomainJsonDecoder,
|
||||
DomainJsonEncoder,
|
||||
JsValueToApiValueConverter,
|
||||
JsValueToApiValueConverter
|
||||
}
|
||||
import com.digitalasset.http.util.ApiValueToLfValueConverter
|
||||
import com.digitalasset.util.ExceptionOps._
|
||||
@ -32,7 +32,7 @@ import com.digitalasset.ledger.client.LedgerClient
|
||||
import com.digitalasset.ledger.client.configuration.{
|
||||
CommandClientConfiguration,
|
||||
LedgerClientConfiguration,
|
||||
LedgerIdRequirement,
|
||||
LedgerIdRequirement
|
||||
}
|
||||
import com.digitalasset.ledger.client.services.pkg.PackageClient
|
||||
import com.digitalasset.ledger.service.LedgerReader
|
||||
@ -126,7 +126,7 @@ object HttpService extends StrictLogging {
|
||||
contractDao,
|
||||
)
|
||||
|
||||
partiesService = new PartiesService(() => client.partyManagementClient.listKnownParties())
|
||||
partiesService = new PartiesService(LedgerClientJwt.listKnownParties(client))
|
||||
|
||||
(encoder, decoder) = buildJsonCodecs(ledgerId, packageService)
|
||||
|
||||
|
@ -6,6 +6,7 @@ package com.digitalasset.http
|
||||
import akka.NotUsed
|
||||
import akka.stream.scaladsl.Source
|
||||
import com.digitalasset.jwt.domain.Jwt
|
||||
import com.digitalasset.ledger.api
|
||||
import com.digitalasset.ledger.api.v1.active_contracts_service.GetActiveContractsResponse
|
||||
import com.digitalasset.ledger.api.v1.command_service.{
|
||||
SubmitAndWaitForTransactionResponse,
|
||||
@ -36,6 +37,9 @@ object LedgerClientJwt {
|
||||
type GetCreatesAndArchivesSince =
|
||||
(Jwt, TransactionFilter, LedgerOffset, Terminates) => Source[Transaction, NotUsed]
|
||||
|
||||
type ListKnownParties =
|
||||
Jwt => Future[List[api.domain.PartyDetails]]
|
||||
|
||||
private def bearer(jwt: Jwt): Some[String] = Some(s"Bearer ${jwt.value: String}")
|
||||
|
||||
def submitAndWaitForTransaction(client: LedgerClient): SubmitAndWaitForTransaction =
|
||||
@ -97,4 +101,7 @@ object LedgerClientJwt {
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
def listKnownParties(client: LedgerClient): ListKnownParties =
|
||||
jwt => client.partyManagementClient.listKnownParties(bearer(jwt))
|
||||
}
|
||||
|
@ -3,14 +3,48 @@
|
||||
|
||||
package com.digitalasset.http
|
||||
|
||||
import com.digitalasset.ledger.api.domain.PartyDetails
|
||||
import com.digitalasset.jwt.domain.Jwt
|
||||
import com.digitalasset.ledger.api
|
||||
import scalaz.OneAnd
|
||||
|
||||
import scala.collection.breakOut
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
class PartiesService(listAllParties: () => Future[List[PartyDetails]])(
|
||||
class PartiesService(listAllParties: LedgerClientJwt.ListKnownParties)(
|
||||
implicit ec: ExecutionContext) {
|
||||
|
||||
def allParties(): Future[List[domain.PartyDetails]] = {
|
||||
listAllParties().map(ps => ps.map(p => domain.PartyDetails.fromLedgerApi(p)))
|
||||
// TODO(Leo) memoize this calls or listAllParties()?
|
||||
def allParties(jwt: Jwt): Future[List[domain.PartyDetails]] = {
|
||||
listAllParties(jwt).map(ps => ps.map(p => domain.PartyDetails.fromLedgerApi(p)))
|
||||
}
|
||||
|
||||
// TODO(Leo) memoize this calls or listAllParties()?
|
||||
def parties(
|
||||
jwt: Jwt,
|
||||
identifiers: OneAnd[Set, domain.Party]
|
||||
): Future[(Set[domain.PartyDetails], Set[domain.Party])] = {
|
||||
val requested: Set[domain.Party] = identifiers.tail + identifiers.head
|
||||
val strIds: Set[String] = domain.Party.unsubst(requested)
|
||||
|
||||
listAllParties(jwt).map { ps =>
|
||||
val result: Set[domain.PartyDetails] = collectParties(ps, strIds)
|
||||
(result, findUnknownParties(result, requested))
|
||||
}
|
||||
}
|
||||
|
||||
private def collectParties(
|
||||
xs: List[api.domain.PartyDetails],
|
||||
requested: Set[String]
|
||||
): Set[domain.PartyDetails] =
|
||||
xs.collect {
|
||||
case p if requested(p.party) => domain.PartyDetails.fromLedgerApi(p)
|
||||
}(breakOut)
|
||||
|
||||
private def findUnknownParties(
|
||||
found: Set[domain.PartyDetails],
|
||||
requested: Set[domain.Party]
|
||||
): Set[domain.Party] =
|
||||
if (found.size == requested.size) Set.empty[domain.Party]
|
||||
else requested -- found.map(_.identifier)
|
||||
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ object domain {
|
||||
queries: NonEmptyList[GetActiveContractsRequest]
|
||||
)
|
||||
|
||||
case class PartyDetails(party: Party, displayName: Option[String], isLocal: Boolean)
|
||||
case class PartyDetails(identifier: Party, displayName: Option[String], isLocal: Boolean)
|
||||
|
||||
final case class CommandMeta(
|
||||
commandId: Option[CommandId],
|
||||
@ -492,7 +492,7 @@ object domain {
|
||||
|
||||
final case class OkResponse[R, W](
|
||||
result: R,
|
||||
warnings: Option[W],
|
||||
warnings: Option[W] = Option.empty[W],
|
||||
status: StatusCode = StatusCodes.OK,
|
||||
) extends ServiceResponse
|
||||
|
||||
@ -521,4 +521,6 @@ object domain {
|
||||
|
||||
final case class UnknownTemplateIds(unknownTemplateIds: List[TemplateId.OptionalPkg])
|
||||
extends ServiceWarning
|
||||
|
||||
final case class UnknownParties(unknownParties: List[domain.Party]) extends ServiceWarning
|
||||
}
|
||||
|
@ -146,14 +146,6 @@ object JsonProtocol extends DefaultJsonProtocol {
|
||||
deserializationError(s"$what requires either key or contractId field")
|
||||
}
|
||||
|
||||
// implicit val ContractLookupRequestFormat
|
||||
// : RootJsonReader[domain.ContractLookupRequest[JsValue]] = {
|
||||
// case JsObject(fields) =>
|
||||
// val id = decodeContractRef(fields, "ContractLookupRequest")
|
||||
// domain.ContractLookupRequest(id)
|
||||
// case _ => deserializationError("ContractLookupRequest must be an object")
|
||||
// }
|
||||
|
||||
implicit val EnrichedContractKeyFormat: RootJsonFormat[domain.EnrichedContractKey[JsValue]] =
|
||||
jsonFormat2(domain.EnrichedContractKey.apply[JsValue])
|
||||
|
||||
@ -288,8 +280,8 @@ object JsonProtocol extends DefaultJsonProtocol {
|
||||
override def write(obj: StatusCode): JsValue = JsNumber(obj.intValue)
|
||||
}
|
||||
|
||||
implicit val OkResponseFormat: RootJsonFormat[domain.OkResponse[JsValue, JsValue]] = jsonFormat3(
|
||||
domain.OkResponse[JsValue, JsValue])
|
||||
implicit def OkResponseFormat[A: JsonFormat, B: JsonFormat]
|
||||
: RootJsonFormat[domain.OkResponse[A, B]] = jsonFormat3(domain.OkResponse[A, B])
|
||||
|
||||
implicit val ErrorResponseFormat: RootJsonFormat[domain.ErrorResponse[JsValue]] = jsonFormat2(
|
||||
domain.ErrorResponse[JsValue])
|
||||
@ -299,15 +291,22 @@ object JsonProtocol extends DefaultJsonProtocol {
|
||||
override def read(json: JsValue): domain.ServiceWarning = json match {
|
||||
case JsObject(fields) if fields.contains("unknownTemplateIds") =>
|
||||
UnknownTemplateIdsFormat.read(json)
|
||||
case JsObject(fields) if fields.contains("unknownParties") =>
|
||||
UnknownPartiesFormat.read(json)
|
||||
case _ =>
|
||||
deserializationError(s"Expected JsObject(unknownTemplateIds -> JsArray(...)), got: $json")
|
||||
deserializationError(
|
||||
s"Expected JsObject(unknownTemplateIds | unknownParties -> JsArray(...)), got: $json")
|
||||
}
|
||||
|
||||
override def write(obj: domain.ServiceWarning): JsValue = obj match {
|
||||
case x: domain.UnknownTemplateIds => UnknownTemplateIdsFormat.write(x)
|
||||
case x: domain.UnknownParties => UnknownPartiesFormat.write(x)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val UnknownTemplateIdsFormat: RootJsonFormat[domain.UnknownTemplateIds] = jsonFormat1(
|
||||
domain.UnknownTemplateIds)
|
||||
|
||||
implicit val UnknownPartiesFormat: RootJsonFormat[domain.UnknownParties] = jsonFormat1(
|
||||
domain.UnknownParties)
|
||||
}
|
||||
|
@ -4,9 +4,13 @@
|
||||
package com.digitalasset.http.json
|
||||
|
||||
import com.digitalasset.util.ExceptionOps._
|
||||
import scalaz.{-\/, Show, \/, \/-}
|
||||
import scalaz.syntax.bitraverse._
|
||||
import scalaz.syntax.traverse._
|
||||
import scalaz.{-\/, Bitraverse, Show, Traverse, \/, \/-}
|
||||
import spray.json.{JsValue, JsonReader, _}
|
||||
|
||||
import scala.language.higherKinds
|
||||
|
||||
@SuppressWarnings(Array("org.wartremover.warts.Any"))
|
||||
object SprayJson {
|
||||
sealed abstract class Error extends Product with Serializable
|
||||
@ -44,6 +48,25 @@ object SprayJson {
|
||||
def decode[A: JsonReader](a: JsValue): JsonReaderError \/ A =
|
||||
\/.fromTryCatchNonFatal(a.convertTo[A]).leftMap(e => JsonReaderError(a.toString, e.description))
|
||||
|
||||
def decode1[F[_], A](a: JsValue)(
|
||||
implicit ev1: JsonReader[F[JsValue]],
|
||||
ev2: Traverse[F],
|
||||
ev3: JsonReader[A]): JsonReaderError \/ F[A] =
|
||||
for {
|
||||
fj <- decode[F[JsValue]](a)
|
||||
fa <- fj.traverse(decode[A](_))
|
||||
} yield fa
|
||||
|
||||
def decode2[F[_, _], A, B](a: JsValue)(
|
||||
implicit ev1: JsonReader[F[JsValue, JsValue]],
|
||||
ev2: Bitraverse[F],
|
||||
ev3: JsonReader[A],
|
||||
ev4: JsonReader[B]): JsonReaderError \/ F[A, B] =
|
||||
for {
|
||||
fjj <- decode[F[JsValue, JsValue]](a)
|
||||
fab <- fjj.bitraverse(decode[A](_), decode[B](_))
|
||||
} yield fab
|
||||
|
||||
def encode[A: JsonWriter](a: A): JsonWriterError \/ JsValue = {
|
||||
import spray.json._
|
||||
\/.fromTryCatchNonFatal(a.toJson).leftMap(e => JsonWriterError(a, e.description))
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
package com.digitalasset.http.util
|
||||
|
||||
import scalaz.{NonEmptyList, \/}
|
||||
import scalaz.{NonEmptyList, OneAnd, \/}
|
||||
|
||||
import scala.collection.TraversableLike
|
||||
|
||||
@ -31,4 +31,9 @@ object Collections {
|
||||
def collect[B](f: A PartialFunction B): Option[NonEmptyList[B]] =
|
||||
self.list.collect(f).toNel
|
||||
}
|
||||
|
||||
def toNonEmptySet[A](as: NonEmptyList[A]): OneAnd[Set, A] = {
|
||||
import scalaz.syntax.foldable._
|
||||
OneAnd(as.head, as.tail.toSet - as.head)
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ import com.digitalasset.grpc.adapter.{AkkaExecutionSequencerPool, ExecutionSeque
|
||||
import com.digitalasset.http.HttpServiceTestFixture.jsonCodecs
|
||||
import com.digitalasset.http.domain.ContractId
|
||||
import com.digitalasset.http.domain.TemplateId.OptionalPkg
|
||||
import com.digitalasset.http.json.SprayJson.objectField
|
||||
import com.digitalasset.http.json.SprayJson.{decode2, objectField}
|
||||
import com.digitalasset.http.json._
|
||||
import com.digitalasset.http.util.ClientUtil.boxedRecord
|
||||
import com.digitalasset.http.util.FutureUtil.toFuture
|
||||
@ -83,11 +83,15 @@ trait AbstractHttpServiceIntegrationTestFuns extends StrictLogging {
|
||||
import shapeless.tag, tag.@@ // used for subtyping to make `AHS ec` beat executionContext
|
||||
implicit val `AHS ec`: ExecutionContext @@ this.type = tag[this.type](`AHS asys`.dispatcher)
|
||||
|
||||
protected def withHttpService[A]
|
||||
: ((Uri, DomainJsonEncoder, DomainJsonDecoder) => Future[A]) => Future[A] =
|
||||
protected def withHttpServiceAndClient[A]
|
||||
: ((Uri, DomainJsonEncoder, DomainJsonDecoder, LedgerClient) => Future[A]) => Future[A] =
|
||||
HttpServiceTestFixture
|
||||
.withHttpService[A](testId, List(dar1, dar2), jdbcConfig, staticContentConfig)
|
||||
|
||||
protected def withHttpService[A](
|
||||
f: (Uri, DomainJsonEncoder, DomainJsonDecoder) => Future[A]): Future[A] =
|
||||
withHttpServiceAndClient((a, b, c, _) => f(a, b, c))
|
||||
|
||||
protected def withLedger[A]: (LedgerClient => Future[A]) => Future[A] =
|
||||
HttpServiceTestFixture.withLedger[A](List(dar1, dar2), testId)
|
||||
|
||||
@ -736,31 +740,86 @@ abstract class AbstractHttpServiceIntegrationTest
|
||||
}: Future[Assertion]
|
||||
}
|
||||
|
||||
"parties endpoint should return all known parties" in withHttpService { (uri, encoder, _) =>
|
||||
val create: domain.CreateCommand[v.Record] = iouCreateCommand()
|
||||
postCreateCommand(create, encoder, uri)
|
||||
.flatMap {
|
||||
case (createStatus, createOutput) =>
|
||||
createStatus shouldBe StatusCodes.OK
|
||||
assertStatus(createOutput, StatusCodes.OK)
|
||||
getRequest(uri = uri.withPath(Uri.Path("/v1/parties")))
|
||||
.flatMap {
|
||||
case (status, output) =>
|
||||
status shouldBe StatusCodes.OK
|
||||
assertStatus(output, StatusCodes.OK)
|
||||
inside(output) {
|
||||
case JsObject(fields) =>
|
||||
inside(fields.get("result")) {
|
||||
case Some(jsArray) =>
|
||||
inside(SprayJson.decode[List[domain.PartyDetails]](jsArray)) {
|
||||
case \/-(partyDetails) =>
|
||||
val partyNames: Set[String] =
|
||||
partyDetails.map(_.party.unwrap)(breakOut)
|
||||
partyNames should contain("Alice")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"parties endpoint should return all known parties" in withHttpServiceAndClient {
|
||||
(uri, _, _, client) =>
|
||||
import scalaz.std.vector._
|
||||
val partyIds = Vector("Alice", "Bob", "Charlie", "Dave")
|
||||
val partyManagement = client.partyManagementClient
|
||||
|
||||
partyIds
|
||||
.traverse { p =>
|
||||
partyManagement.allocateParty(Some(p), Some(s"$p & Co. LLC"))
|
||||
}
|
||||
.flatMap { allocatedParties =>
|
||||
getRequest(uri = uri.withPath(Uri.Path("/v1/parties"))).flatMap {
|
||||
case (status, output) =>
|
||||
status shouldBe StatusCodes.OK
|
||||
inside(
|
||||
decode2[domain.OkResponse, List[domain.PartyDetails], Unit](output)
|
||||
) {
|
||||
case \/-(response) =>
|
||||
response.status shouldBe StatusCodes.OK
|
||||
response.warnings shouldBe None
|
||||
val actualIds: Set[domain.Party] = response.result.map(_.identifier)(breakOut)
|
||||
actualIds shouldBe domain.Party.subst(partyIds.toSet)
|
||||
response.result.toSet shouldBe
|
||||
allocatedParties.toSet.map(domain.PartyDetails.fromLedgerApi)
|
||||
}
|
||||
}
|
||||
}: Future[Assertion]
|
||||
}
|
||||
|
||||
"parties endpoint should return only requested parties, unknown parties returned as warnings" in withHttpServiceAndClient {
|
||||
(uri, _, _, client) =>
|
||||
import scalaz.std.vector._
|
||||
|
||||
val charlie = domain.Party("Charlie")
|
||||
val knownParties = domain.Party.subst(Vector("Alice", "Bob", "Dave")) :+ charlie
|
||||
val erin = domain.Party("Erin")
|
||||
val requestedPartyIds: Vector[domain.Party] = knownParties.filterNot(_ == charlie) :+ erin
|
||||
|
||||
val partyManagement = client.partyManagementClient
|
||||
|
||||
knownParties
|
||||
.traverse { p =>
|
||||
partyManagement.allocateParty(Some(p.unwrap), Some(s"${p.unwrap} & Co. LLC"))
|
||||
}
|
||||
.flatMap { allocatedParties =>
|
||||
postJsonRequest(
|
||||
uri = uri.withPath(Uri.Path("/v1/parties")),
|
||||
JsArray(requestedPartyIds.map(x => JsString(x.unwrap)))
|
||||
).flatMap {
|
||||
case (status, output) =>
|
||||
status shouldBe StatusCodes.OK
|
||||
inside(
|
||||
decode2[domain.OkResponse, List[domain.PartyDetails], domain.UnknownParties](output)
|
||||
) {
|
||||
case \/-(response) =>
|
||||
response.status shouldBe StatusCodes.OK
|
||||
response.warnings shouldBe Some(domain.UnknownParties(List(erin)))
|
||||
val actualIds: Set[domain.Party] = response.result.map(_.identifier)(breakOut)
|
||||
actualIds shouldBe requestedPartyIds.toSet - erin // Erin is not known
|
||||
val expected: Set[domain.PartyDetails] = allocatedParties.toSet
|
||||
.map(domain.PartyDetails.fromLedgerApi)
|
||||
.filterNot(_.identifier == charlie)
|
||||
response.result.toSet shouldBe expected
|
||||
}
|
||||
}
|
||||
}: Future[Assertion]
|
||||
}
|
||||
|
||||
"parties endpoint should error if empty array passed as input" in withHttpServiceAndClient {
|
||||
(uri, _, _, _) =>
|
||||
postJsonRequest(
|
||||
uri = uri.withPath(Uri.Path("/v1/parties")),
|
||||
JsArray(Vector.empty)
|
||||
).flatMap {
|
||||
case (status, output) =>
|
||||
status shouldBe StatusCodes.BadRequest
|
||||
assertStatus(output, StatusCodes.BadRequest)
|
||||
val errorMsg = expectedOneErrorMessage(output)
|
||||
errorMsg should include("Cannot read JSON: <[]>")
|
||||
errorMsg should include("must be a list with at least 1 element")
|
||||
}: Future[Assertion]
|
||||
}
|
||||
|
||||
|
@ -45,7 +45,7 @@ object HttpServiceTestFixture {
|
||||
dars: List[File],
|
||||
jdbcConfig: Option[JdbcConfig],
|
||||
staticContentConfig: Option[StaticContentConfig]
|
||||
)(testFn: (Uri, DomainJsonEncoder, DomainJsonDecoder) => Future[A])(
|
||||
)(testFn: (Uri, DomainJsonEncoder, DomainJsonDecoder, LedgerClient) => Future[A])(
|
||||
implicit asys: ActorSystem,
|
||||
mat: Materializer,
|
||||
aesf: ExecutionSequencerFactory,
|
||||
@ -92,8 +92,9 @@ object HttpServiceTestFixture {
|
||||
val fa: Future[A] = for {
|
||||
(_, httpPort) <- httpServiceF
|
||||
(encoder, decoder) <- codecsF
|
||||
client <- clientF
|
||||
uri = Uri.from(scheme = "http", host = "localhost", port = httpPort)
|
||||
a <- testFn(uri, encoder, decoder)
|
||||
a <- testFn(uri, encoder, decoder, client)
|
||||
} yield a
|
||||
|
||||
fa.onComplete { _ =>
|
||||
|
Loading…
Reference in New Issue
Block a user