Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4b0223e
feature/Add in-memory offer service and related models, implementing …
hongwei1 Oct 16, 2025
6018a9d
refactor/Update .gitignore to simplify path for metals.sbt, improving…
hongwei1 Oct 16, 2025
e500b51
test/Add unit tests for OfferRoutes, covering create, retrieve, and c…
hongwei1 Oct 16, 2025
34b36cd
feature/Add initial implementation of trading documentation models an…
hongwei1 Nov 27, 2025
d1a9b76
feature/Add server configuration to application.conf.example and upda…
hongwei1 Nov 28, 2025
ecefddd
feature/Add HTTP package object with constants, type aliases, and uti…
hongwei1 Nov 28, 2025
181eab4
refactor/Update error handling in services and routes to use standard…
hongwei1 Nov 28, 2025
0e8d7b9
feature/Refactor ResourceDoc and introduce EmptyBody marker for impro…
hongwei1 Dec 1, 2025
99d264d
refactor/Simplify ResourceDoc and service definitions by removing exi…
hongwei1 Dec 1, 2025
80155d3
feature/Add ImplementedByJson and ResourceDocJson case classes to enh…
hongwei1 Dec 1, 2025
c1d0d00
feature/Enhance API documentation by adding resource documentation en…
hongwei1 Dec 1, 2025
bfa60dd
refactor/Update partial function names and improve consistency in API…
hongwei1 Dec 1, 2025
50ca13c
feature/Add detailed examples for trading offer API responses, enhanc…
hongwei1 Dec 1, 2025
71036c2
refactor/Streamline JSON serialization for ObpOfferResponseExample by…
hongwei1 Dec 1, 2025
e087564
feature/Add ResourceDocMeta and ResourceDocsJson case classes to enha…
hongwei1 Dec 2, 2025
5756c2c
feature/Add JSON Schema support for API responses by integrating scal…
hongwei1 Dec 2, 2025
338c0a7
refactor/Update ResourceDoc and Routes to use Option types for exampl…
hongwei1 Dec 2, 2025
406d433
refactor/Enhance JSON encoding for ResourceDocJson by dropping null v…
hongwei1 Dec 2, 2025
d245013
fix/Update library dependencies for JSON schema integration to use th…
hongwei1 Dec 2, 2025
33293e0
docs/Add deployment instructions to README for building and running t…
hongwei1 Dec 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,6 @@ build/

*.code-workspace
.specstory
project/project/metals.sbt
project/metals.sbt
metals.sbt
.bsp/sbt.json
*.conf
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,20 @@ connectors {

See `ai.log` for detailed development history and architectural decisions.


## Deployment

1. Build the assembly JAR:
```bash
sbt assembly
```
2. Copy the JAR from `target/scala-2.13/obp-trading-assembly-0.1.0-SNAPSHOT.jar` to your server.
3. Run with Java 11+:
```bash
java -jar obp-trading-assembly-0.1.0-SNAPSHOT.jar
```


## License

AGPL-3.0
Expand Down
5 changes: 5 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ libraryDependencies ++= Seq(
"io.circe" %% "circe-generic" % "0.14.6",
"io.circe" %% "circe-parser" % "0.14.6",
"io.circe" %% "circe-literal" % "0.14.6",

// JSON Schema generation
"com.github.andyglow" %% "scala-jsonschema" % "0.7.11",
"com.github.andyglow" %% "scala-jsonschema-circe-json" % "0.7.11",


// Database
"com.typesafe.slick" %% "slick" % slickVersion,
Expand Down
5 changes: 5 additions & 0 deletions src/main/resources/application.conf.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
// Example configuration for OBP-Trading
// Copy to application.conf and adjust values for your environment

server {
host = "0.0.0.0" // HTTP server host
port = "8086" // HTTP server port
}

obp {
api {
base_url = "https://api.openbankproject.com" // OBP-API base URL
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.openbankproject.trading.docs

import com.openbankproject.trading.docs.model.ResourceDoc
import com.openbankproject.trading.docs.registry.ResourceDocRegistry

object TradingResourceDocs {

private val docs: Seq[ResourceDoc] = Seq.empty

def registerAll(): Unit = ResourceDocRegistry.registerAll(docs)
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.openbankproject.trading.docs.model

/**
* Marker for empty request/response body in ResourceDoc examples.
*/
case object EmptyBody extends Product with Serializable {
override def productArity: Int = 0
override def productElement(n: Int): Any = throw new IndexOutOfBoundsException(n.toString)
override def canEqual(that: Any): Boolean = that.isInstanceOf[EmptyBody.type]
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.openbankproject.trading.docs.model

final case class ErrorDoc(code: String, httpStatus: Int, message: Option[String] = None)


Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.openbankproject.trading.docs.model

sealed trait HttpMethod extends Product with Serializable { def name: String }

object HttpMethod {
case object GET extends HttpMethod { val name: String = "GET" }
case object POST extends HttpMethod { val name: String = "POST" }
case object PUT extends HttpMethod { val name: String = "PUT" }
case object DELETE extends HttpMethod { val name: String = "DELETE" }
case object PATCH extends HttpMethod { val name: String = "PATCH" }
case object HEAD extends HttpMethod { val name: String = "HEAD" }
case object OPTIONS extends HttpMethod { val name: String = "OPTIONS" }
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.openbankproject.trading.docs.model

sealed trait RequiredRole extends Product with Serializable

object RequiredRole {
case object Public extends RequiredRole {
override def toString: String = "Public"
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package com.openbankproject.trading.docs.model

import com.openbankproject.trading.http.OBPEndpoint
import io.circe.Json
import java.time.Instant

/**
* ResourceDoc aligned with OBP-API structure.
* Simplified version without Lift dependencies.
*
* @param partialFunction The actual partial function implementing this endpoint
* @param implementedInApiVersion API version (e.g., "v7.0.0")
* @param partialFunctionName Name of the partial function (e.g., "getOfferPF")
* @param requestVerb HTTP method (GET, POST, PUT, DELETE, etc.)
* @param requestUrl URL pattern with path parameters
* @param summary Short description of the endpoint
* @param description Detailed description
* @param exampleRequestBody Example request body as case class (use EmptyBody if no body)
* @param successResponseBody Example success response as case class
* @param errorResponseBodies List of possible error messages
* @param tags Tags for categorization
* @param roles Required roles (None means public)
*/
final case class ResourceDoc(
partialFunction: OBPEndpoint,
implementedInApiVersion: String,
partialFunctionName: String,
requestVerb: String,
requestUrl: String,
summary: String,
description: String,
exampleRequestBody: Option[Product] = None,
successResponseBody: Product,
errorResponseBodies: List[String],
tags: List[String],
roles: Option[List[String]] = None,
isFeatured: Boolean = false,
specialInstructions: Option[String] = None,
specifiedUrl: String = "",
createdByBankId: Option[String] = None
) {
require(partialFunctionName.trim.nonEmpty, "partialFunctionName must be non-empty")
require(requestUrl.trim.nonEmpty, "requestUrl must be non-empty")
require(requestVerb.trim.nonEmpty, "requestVerb must be non-empty")
}

/**
* Used to describe where an API call is implemented, similar to OBP's ImplementedByJson.
*/
final case class ImplementedByJson(
version: String, // Short hand for version, e.g. "v7_0_0"
function: String // The partial function name, e.g. "getOfferPF"
)

/**
* Export-friendly ResourceDocJson, mirroring OBP-API structure for docs/export.
*/

/**
* Metadata summary for exported ResourceDocs.
*/
final case class ResourceDocMeta(
response_date: Instant,
count: Int
)

/**
* Wrapper for exported docs list and metadata.
*/
final case class ResourceDocsJson(
resource_docs: List[ResourceDocJson],
meta: Option[ResourceDocMeta] = None
)

final case class ResourceDocJson(
operation_id: String,
implemented_by: ImplementedByJson,
request_verb: String,
request_url: String,
summary: String,
description: String,
description_markdown: String,
example_request_body: Option[Json],
success_response_body: Json,
error_response_bodies: List[String],
tags: List[String],
typed_request_body: Option[Json],
typed_success_response_body: Option[Json],
roles: Option[List[String]] = None,
is_featured: Boolean = false,
special_instructions: Option[String] = None,
specified_url: String,
connector_methods: List[String] = Nil,
created_by_bank_id: Option[String] = None
)

object ResourceDocJson {
import io.circe.generic.semiauto._
import io.circe.{Encoder, Decoder}

implicit val implementedByJsonEncoder: Encoder[ImplementedByJson] =
deriveEncoder[ImplementedByJson]
implicit val implementedByJsonDecoder: Decoder[ImplementedByJson] =
deriveDecoder[ImplementedByJson]

implicit val resourceDocMetaEncoder: Encoder[ResourceDocMeta] =
deriveEncoder[ResourceDocMeta]
implicit val resourceDocMetaDecoder: Decoder[ResourceDocMeta] =
deriveDecoder[ResourceDocMeta]

implicit val resourceDocJsonEncoder: Encoder[ResourceDocJson] =
deriveEncoder[ResourceDocJson].mapJson(_.dropNullValues)
implicit val resourceDocJsonDecoder: Decoder[ResourceDocJson] =
deriveDecoder[ResourceDocJson]

implicit val resourceDocsJsonEncoder: Encoder[ResourceDocsJson] =
deriveEncoder[ResourceDocsJson]
implicit val resourceDocsJsonDecoder: Decoder[ResourceDocsJson] =
deriveDecoder[ResourceDocsJson]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.openbankproject.trading.docs.registry

import com.openbankproject.trading.docs.model.ResourceDoc

object ConsistencyCheck {
def findDuplicateImplementations(docs: Seq[ResourceDoc]): Map[String, Int] =
docs.groupBy(_.partialFunctionName).view.mapValues(_.size).filter(_._2 > 1).toMap
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.openbankproject.trading.docs.registry

import com.openbankproject.trading.docs.model.ResourceDoc
import scala.collection.concurrent.TrieMap

object ResourceDocRegistry {
private[this] val byPartialFunctionName: TrieMap[String, ResourceDoc] = TrieMap.empty

def register(doc: ResourceDoc): Unit = {
byPartialFunctionName.put(doc.partialFunctionName, doc)
()
}

def registerAll(docs: Iterable[ResourceDoc]): Unit = docs.foreach(register)

def get(partialFunctionName: String): Option[ResourceDoc] =
byPartialFunctionName.get(partialFunctionName)

def all: Vector[ResourceDoc] =
byPartialFunctionName.values.toVector.sortBy(_.partialFunctionName)

def clear(): Unit = byPartialFunctionName.clear()
}


Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,46 @@ import org.http4s.server.Router
import org.http4s.implicits._
import org.http4s.ember.server.EmberServerBuilder
import com.comcast.ip4s._
import com.typesafe.config.ConfigFactory

/**
* Minimal http4s server that mounts Routes.api with placeholder services.
* Replace the placeholder services with real implementations when ready.
*/
object HttpServerMain extends IOApp.Simple {

private val orderServiceIO: IO[OrderService[IO]] = InMemoryOrderService.create[IO]()
private val orderServiceIO: IO[OrderService] = InMemoryOrderService.create()
private val offerServiceIO: IO[OfferService] = InMemoryOfferService.create()

private val matchService: MatchService[IO] = new MatchService[IO] {
def createMatch(req: MatchRequest) = IO.pure(Left(ErrorResponse("not_implemented", "createMatch not implemented")))
private val matchService: MatchService = new MatchService {
def createMatch(req: MatchRequest) = IO.pure(Left(ErrorResponse(ErrorCodes.NOT_IMPLEMENTED, "createMatch not implemented")))
}

private val settlementService: SettlementService[IO] = new SettlementService[IO] {
def settle(req: SettlementRequest) = IO.pure(Left(ErrorResponse("not_implemented", "settle not implemented")))
def getTrade(tradeId: String) = IO.pure(Left(ErrorResponse("not_implemented", "getTrade not implemented")))
private val settlementService: SettlementService = new SettlementService {
def settle(req: SettlementRequest) = IO.pure(Left(ErrorResponse(ErrorCodes.NOT_IMPLEMENTED, "settle not implemented")))
def getTrade(tradeId: String) = IO.pure(Left(ErrorResponse(ErrorCodes.NOT_IMPLEMENTED, "getTrade not implemented")))
}

private val fundsService: FundsService[IO] = new FundsService[IO] {
def notifyDeposit(req: DepositNotification) = IO.pure(Left(ErrorResponse("not_implemented", "notifyDeposit not implemented")))
def requestWithdrawal(req: WithdrawalRequest) = IO.pure(Left(ErrorResponse("not_implemented", "requestWithdrawal not implemented")))
private val fundsService: FundsService = new FundsService {
def notifyDeposit(req: DepositNotification) = IO.pure(Left(ErrorResponse(ErrorCodes.NOT_IMPLEMENTED, "notifyDeposit not implemented")))
def requestWithdrawal(req: WithdrawalRequest) = IO.pure(Left(ErrorResponse(ErrorCodes.NOT_IMPLEMENTED, "requestWithdrawal not implemented")))
}

override def run: IO[Unit] =
for {
config <- IO(ConfigFactory.load())
serverHost = config.getString("server.host")
serverPort = config.getString("server.port").toInt
offerService <- offerServiceIO
orderService <- orderServiceIO
apiRoutes = Routes.api[IO](orderService, matchService, settlementService, fundsService)
apiRoutes = Routes.api(orderService, offerService, matchService, settlementService, fundsService)
httpApp = Router("/" -> apiRoutes).orNotFound
host <- IO.fromOption(Host.fromString(serverHost))(new IllegalArgumentException(s"Invalid host: $serverHost"))
port <- IO.fromOption(Port.fromInt(serverPort))(new IllegalArgumentException(s"Invalid port: $serverPort"))
_ <- EmberServerBuilder
.default[IO]
.withHost(ipv4"0.0.0.0")
.withPort(port"8080")
.withHost(host)
.withPort(port)
.withHttpApp(httpApp)
.build
.useForever
Expand Down
Loading