Skip to content

Commit

Permalink
CHANGELOG_BEGIN
Browse files Browse the repository at this point in the history
[JSON API - Experimental]
``/contracts/search`` endpoint reports unresolved template IDs as warnings, see #3771::

    {
        "warnings": {
            "unknownTemplateIds": ["UnknownModule:UnknownEntity"]
        },
        "result": [{...}, ...],
        "status": 200
    }

CHANGELOG_END
  • Loading branch information
leo-da committed Jan 10, 2020
1 parent ec0897b commit 4f39331
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 79 deletions.
30 changes: 30 additions & 0 deletions docs/source/json-api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,36 @@ Each contract formatted according to :doc:`lf-value-specification`.
"status": 200
}
Nonempty Response with Unknown Template IDs Warning
---------------------------------------------------

.. code-block:: json
{
"warnings": {
"unknownTemplateIds": ["UnknownModule:UnknownEntity"]
},
"result": [
{
"observers": [],
"agreementText": "",
"payload": {
"observers": [],
"issuer": "Alice",
"amount": "999.99",
"currency": "USD",
"owner": "Alice"
},
"signatories": [
"Alice"
],
"contractId": "#52:0",
"templateId": "b10d22d6c2f2fae41b353315cf893ed66996ecb0abe4424ea6a81576918f658a:Iou:Iou"
}
],
"status": 200
}
POST ``/command/create``
========================

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ import com.digitalasset.http.json.JsonProtocol.LfValueCodec
import com.digitalasset.http.query.ValuePredicate
import com.digitalasset.http.query.ValuePredicate.LfV
import com.digitalasset.http.util.ApiValueToLfValueConverter
import com.digitalasset.util.ExceptionOps._
import com.digitalasset.http.util.FutureUtil.toFuture
import com.digitalasset.http.util.IdentifierConverters.apiIdentifier
import com.digitalasset.jwt.domain.Jwt
import com.digitalasset.ledger.api.refinements.{ApiTypes => lar}
import com.digitalasset.ledger.api.{v1 => api}
import com.digitalasset.util.ExceptionOps._
import com.typesafe.scalalogging.StrictLogging
import scalaz.syntax.show._
import scalaz.syntax.std.option._
Expand Down Expand Up @@ -122,35 +122,37 @@ class ContractsService(

def retrieveAll(
jwt: Jwt,
jwtPayload: JwtPayload): Source[Error \/ domain.ActiveContract[LfValue], NotUsed] =
jwtPayload: JwtPayload): SearchResult[Error \/ domain.ActiveContract[LfValue]] =
retrieveAll(jwt, jwtPayload.party)

def retrieveAll(
jwt: Jwt,
party: domain.Party): Source[Error \/ domain.ActiveContract[LfValue], NotUsed] =
Source(allTemplateIds()).flatMapConcat(x => searchInMemoryOneTpId(jwt, party, x, Map.empty))
party: domain.Party): SearchResult[Error \/ domain.ActiveContract[LfValue]] =
SearchResult(
Source(allTemplateIds()).flatMapConcat(x => searchInMemoryOneTpId(jwt, party, x, Map.empty)),
Set.empty)

def search(jwt: Jwt, jwtPayload: JwtPayload, request: GetActiveContractsRequest)
: Source[Error \/ domain.ActiveContract[JsValue], NotUsed] =
def search(
jwt: Jwt,
jwtPayload: JwtPayload,
request: GetActiveContractsRequest): SearchResult[Error \/ domain.ActiveContract[JsValue]] =
search(jwt, jwtPayload.party, request.templateIds, request.query)

def search(
jwt: Jwt,
party: domain.Party,
templateIds: Set[domain.TemplateId.OptionalPkg],
queryParams: Map[String, JsValue])
: Source[Error \/ domain.ActiveContract[JsValue], NotUsed] = {
queryParams: Map[String, JsValue]): SearchResult[Error \/ domain.ActiveContract[JsValue]] = {

resolveRequiredTemplateIds(templateIds) match {
case -\/(e) =>
Source.single(-\/(Error('search, e.shows)))
case \/-(resolvedTemplateIds) =>
daoAndFetch.cata(
x => searchDb(x._1, x._2)(jwt, party, resolvedTemplateIds.toSet, queryParams),
searchInMemory(jwt, party, resolvedTemplateIds.toSet, queryParams)
.map(_.flatMap(lfAcToJsAc))
)
}
val (resolvedTemplateIds, unresolvedTemplateIds) = resolveTemplateIds(templateIds)

val source = daoAndFetch.cata(
x => searchDb(x._1, x._2)(jwt, party, resolvedTemplateIds, queryParams),
searchInMemory(jwt, party, resolvedTemplateIds, queryParams)
.map(_.flatMap(lfAcToJsAc))
)

SearchResult(source, unresolvedTemplateIds)
}

// we store create arguments as JASON in DB, that is why it is `domain.ActiveContract[JsValue]` in the result
Expand Down Expand Up @@ -304,17 +306,18 @@ class ContractsService(
\/.fromTryCatchNonFatal(LfValueCodec.apiValueToJsValue(a)).leftMap(e =>
Error('lfValueToJsValue, e.description))

@SuppressWarnings(Array("org.wartremover.warts.Any"))
private def resolveRequiredTemplateIds(
xs: Set[domain.TemplateId.OptionalPkg]): Error \/ List[domain.TemplateId.RequiredPkg] = {
import scalaz.std.list._
xs.toList.traverse(resolveRequiredTemplateId)
}
private def resolveTemplateIds(xs: Set[domain.TemplateId.OptionalPkg])
: (Set[domain.TemplateId.RequiredPkg], Set[domain.TemplateId.OptionalPkg]) = {

val z = (Set.empty[domain.TemplateId.RequiredPkg], Set.empty[domain.TemplateId.OptionalPkg])

private def resolveRequiredTemplateId(
x: domain.TemplateId.OptionalPkg): Error \/ domain.TemplateId.RequiredPkg =
resolveTemplateId(x).toRightDisjunction(
Error('resolveRequiredTemplateId, ErrorMessages.cannotResolveTemplateId(x)))
xs.toList.foldLeft(z) { (b, a) =>
resolveTemplateId(a) match {
case Some(x) => (b._1 + x, b._2)
case None => (b._1, b._2 + a)
}
}
}
}

object ContractsService {
Expand All @@ -329,4 +332,9 @@ object ContractsService {
s"ContractService Error, ${e.id: Symbol}: ${e.message: String}"
}
}

final case class SearchResult[A](
source: Source[A, NotUsed],
unresolvedTemplateIds: Set[domain.TemplateId.OptionalPkg]
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@ import akka.NotUsed
import akka.http.scaladsl.model.HttpMethods.{GET, POST}
import akka.http.scaladsl.model._
import akka.http.scaladsl.model.headers.{Authorization, OAuth2BearerToken}
import akka.stream.Materializer
import akka.stream.scaladsl.{Flow, Source}
import com.digitalasset.daml.lf
import com.digitalasset.http.ContractsService.SearchResult
import com.digitalasset.http.EndpointsCompanion._
import com.digitalasset.http.Statement.discard
import com.digitalasset.http.domain.JwtPayload
import com.digitalasset.http.json.{DomainJsonDecoder, DomainJsonEncoder, ResponseFormats, SprayJson}
import com.digitalasset.util.ExceptionOps._
import com.digitalasset.http.util.FutureUtil.{either, eitherT}
import com.digitalasset.http.util.{ApiValueToLfValueConverter, FutureUtil}
import com.digitalasset.jwt.domain.Jwt
import com.digitalasset.ledger.api.refinements.{ApiTypes => lar}
import com.digitalasset.ledger.api.{v1 => lav1}
import com.digitalasset.util.ExceptionOps._
import com.typesafe.scalalogging.StrictLogging
import scalaz.std.scalaFuture._
import scalaz.syntax.show._
Expand All @@ -29,10 +33,6 @@ import scala.concurrent.duration.FiniteDuration
import scala.concurrent.{ExecutionContext, Future}
import scala.util.control.NonFatal

import akka.stream.Materializer
import akka.stream.scaladsl.{Source, Flow}
import com.digitalasset.http.EndpointsCompanion._

@SuppressWarnings(Array("org.wartremover.warts.Any"))
class Endpoints(
ledgerId: lar.LedgerId,
Expand Down Expand Up @@ -160,30 +160,40 @@ class Endpoints(
httpResponse(et)

case req @ HttpRequest(GET, Uri.Path("/contracts/search"), _, _, _) =>
val sourceF: Future[Error \/ Source[Error \/ JsValue, NotUsed]] = input(req).map {
val sourceF: Future[Error \/ SearchResult[Error \/ JsValue]] = input(req).map {
_.map {
case (jwt, jwtPayload, _) =>
contractsService
.retrieveAll(jwt, jwtPayload)
val result: SearchResult[ContractsService.Error \/ domain.ActiveContract[LfValue]] =
contractsService
.retrieveAll(jwt, jwtPayload)

val jsValSource = result.source
.via(handleSourceFailure)
.map(_.flatMap(lfAcToJsValue)): Source[Error \/ JsValue, NotUsed]

result.copy(source = jsValSource): SearchResult[Error \/ JsValue]
}
}

httpResponse(sourceF)

case req @ HttpRequest(POST, Uri.Path("/contracts/search"), _, _, _) =>
val sourceF: Future[Error \/ Source[Error \/ JsValue, NotUsed]] = input(req).map {
val sourceF: Future[Error \/ SearchResult[Error \/ JsValue]] = input(req).map {
_.flatMap {
case (jwt, jwtPayload, reqBody) =>
SprayJson
.decode[domain.GetActiveContractsRequest](reqBody)
.leftMap(e => InvalidUserInput(e.shows))
.map { cmd =>
contractsService
.search(jwt, jwtPayload, cmd)
val result: SearchResult[ContractsService.Error \/ domain.ActiveContract[JsValue]] =
contractsService
.search(jwt, jwtPayload, cmd)

val jsValSource: Source[Error \/ JsValue, NotUsed] = result.source
.via(handleSourceFailure)
.map(_.flatMap(jsAcToJsValue)): Source[Error \/ JsValue, NotUsed]
.map(_.flatMap(jsAcToJsValue))

result.copy(source = jsValSource): SearchResult[Error \/ JsValue]
}
}
}
Expand Down Expand Up @@ -242,22 +252,34 @@ class Endpoints(
}

private def httpResponse(
output: Future[Error \/ Source[Error \/ JsValue, NotUsed]]): Future[HttpResponse] =
output: Future[Error \/ SearchResult[Error \/ JsValue]]): Future[HttpResponse] =
output
.map {
case -\/(e) => httpResponseError(e)
case \/-(source) => httpResponseFromSource(source)
case \/-(searchResult) => httpResponse(searchResult)
}
.recover {
case NonFatal(e) => httpResponseError(ServerError(e.description))
}

private def httpResponseFromSource(data: Source[Error \/ JsValue, NotUsed]): HttpResponse =
private def httpResponse(searchResult: SearchResult[Error \/ JsValue]): HttpResponse = {
val jsValSource: Source[Error \/ JsValue, NotUsed] = searchResult.source

val warnings: Option[domain.UnknownTemplateIds] =
if (searchResult.unresolvedTemplateIds.nonEmpty)
Some(domain.UnknownTemplateIds(searchResult.unresolvedTemplateIds.toList))
else None

val jsValWarnings: Option[JsValue] = warnings.map(_.toJson)

HttpResponse(
status = StatusCodes.OK,
entity = HttpEntity
.CloseDelimited(ContentTypes.`application/json`, ResponseFormats.resultJsObject(data))
.CloseDelimited(
ContentTypes.`application/json`,
ResponseFormats.resultJsObject(jsValSource, jsValWarnings))
)
}

private[http] def input(req: HttpRequest): Future[Unauthorized \/ (Jwt, JwtPayload, String)] = {
findJwt(req).flatMap(decodeAndParsePayload(_, decodeJwt)) match {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,22 @@ private[http] object ResponseFormats {
}

def resultJsObject[E: Show](
jsVals: Source[E \/ JsValue, NotUsed]): Source[ByteString, NotUsed] = {
jsVals: Source[E \/ JsValue, NotUsed],
warnings: Option[JsValue]): Source[ByteString, NotUsed] = {

val graph = GraphDSL.create() { implicit b =>
import GraphDSL.Implicits._

val partition: FanOutShape2[E \/ JsValue, E, JsValue] = b add ContractsFetch.partition
val concat: UniformFanInShape[ByteString, ByteString] = b add Concat(3)

// first produce the result element
Source.single(ByteString("""{"result":[""")) ~> concat.in(0)
// first produce optional warnings and result element
warnings match {
case Some(x) =>
Source.single(ByteString(s"""{"warnings":${x.compactPrint},"result":[""")) ~> concat.in(0)
case None =>
Source.single(ByteString("""{"result":[""")) ~> concat.in(0)
}

jsVals ~> partition.in

Expand Down
Loading

0 comments on commit 4f39331

Please sign in to comment.