Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/PUBLISHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ git tag v1.0.0
git push origin v1.0.0

# GitHub Actions automatically:
# ✅ Builds all Scala versions (2.12.17, 2.13.14, 3.3.1)
# ✅ Builds all Scala versions (2.12.20, 2.13.14, 3.3.1)
# ✅ Runs complete test suite
# ✅ Signs artifacts with GPG
# ✅ Publishes to Maven Central
Expand Down
13 changes: 11 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
strategy:
matrix:
java: [17]
scala: [2.12.17, 2.13.14, 3.3.1]
scala: [2.12.20, 2.13.14, 3.3.1]

steps:
- name: Checkout code
Expand All @@ -26,6 +26,11 @@ jobs:
java-version: ${{ matrix.java }}
distribution: 'temurin'

- name: Install sbt
uses: coursier/setup-action@v1
with:
apps: sbt

- name: Cache SBT
uses: actions/cache@v4
with:
Expand All @@ -44,7 +49,6 @@ jobs:
run: sbt "++${{ matrix.scala }}" test

- name: Check formatting
if: matrix.scala == '2.13.14'
run: sbt scalafmtCheckAll

publish-snapshot:
Expand All @@ -63,6 +67,11 @@ jobs:
java-version: '17'
distribution: 'temurin'

- name: Install sbt
uses: coursier/setup-action@v1
with:
apps: sbt

- name: Cache SBT
uses: actions/cache@v4
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ jobs:
```

### Supported Scala Versions
- Scala 2.12.17
- Scala 2.12.20
- Scala 2.13.14
- Scala 3.3.1

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -320,3 +320,6 @@ temp/
# OS generated files
.DS_Store?
Icon?
.specstory
.cursorindexingignore
OBP-Scala-Library.code-workspace
5 changes: 5 additions & 0 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
version = 3.7.17
runner.dialect = scala213
maxColumn = 120
# Keep default formatting for ScalaDoc and comments (use scalafmt defaults)

6 changes: 3 additions & 3 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ This guide covers Docker-based development for the OBP Scala Library. The Docker
### SBT Operations
```bash
./dev.sh compile # Compile all versions
./dev.sh compile 2.12.17 # Compile specific version
./dev.sh compile 2.12.20 # Compile specific version
./dev.sh test # Run tests for all versions
./dev.sh test 3.3.1 # Run tests for specific version
./dev.sh publish # Publish locally (all versions)
Expand Down Expand Up @@ -75,7 +75,7 @@ implementation 'com.openbankproject:obp-scala-library_2.13:0.1.0-SNAPSHOT'

## Supported Scala Versions

- **2.12.17** - LTS version, widely used in enterprise
- **2.12.20** - LTS version, widely used in enterprise
- **2.13.14** - Current stable version (default)
- **3.3.1** - Latest Scala 3 version

Expand Down Expand Up @@ -153,7 +153,7 @@ sbt

### 1. Update build.sbt
```scala
crossScalaVersions := Seq("2.12.17", "2.13.14", "3.3.1", "3.4.0") // Add new version
crossScalaVersions := Seq("2.12.20", "2.13.14", "3.3.1", "3.4.0") // Add new version
```

### 2. Rebuild Docker image
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ git push origin v1.0.0

## Supported Scala Versions

- **2.12.17** - LTS version
- **2.12.20** - LTS version
- **2.13.14** - Current stable (default)
- **3.3.1** - Latest Scala 3

Expand Down
13 changes: 8 additions & 5 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version := "0.1.0-SNAPSHOT"

scalaVersion := "2.13.14"

crossScalaVersions := Seq("2.12.17", "2.13.14", "3.3.1")
crossScalaVersions := Seq("2.12.20", "2.13.14", "3.3.1")

// Organization and metadata
ThisBuild / organization := "com.openbankproject"
Expand All @@ -13,6 +13,11 @@ ThisBuild / organizationHomepage := Some(url("https://www.openbankproject.com/")

// Dependencies
libraryDependencies ++= Seq(
"io.circe" %% "circe-core" % "0.14.10",
"io.circe" %% "circe-parser" % "0.14.10",
// JSON Schema generation
"com.github.andyglow" %% "scala-jsonschema" % "0.7.11",
"com.github.andyglow" %% "scala-jsonschema-circe-json" % "0.7.11",
"org.scalatest" %% "scalatest" % "3.2.19" % Test
)

Expand Down Expand Up @@ -55,10 +60,8 @@ ThisBuild / developers := List(
)
)

// POM customization for Maven Central
pomExtra := (
<description>Open Bank Project Scala Library - A comprehensive library for banking operations</description>
)
// Project description (will be used in generated POM)
description := "Open Bank Project Scala Library - A comprehensive library for banking operations"

// Cross-compilation settings
crossPaths := true
Expand Down
2 changes: 1 addition & 1 deletion docker/dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Examples:
$0 all # Full build + publish to host Maven repo

Supported Scala Versions:
2.12.17, 2.13.14, 3.3.1
2.12.20, 2.13.14, 3.3.1

EOF
}
Expand Down
2 changes: 1 addition & 1 deletion docker/sbt/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ WORKDIR /tmp/warmup

# Create minimal build.sbt to download SBT dependencies
RUN echo 'scalaVersion := "2.13.14"' > build.sbt && \
echo 'crossScalaVersions := Seq("2.12.17", "2.13.14", "3.3.1")' >> build.sbt && \
echo 'crossScalaVersions := Seq("2.12.20", "2.13.14", "3.3.1")' >> build.sbt && \
echo 'name := "warmup"' >> build.sbt && \
echo 'libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.19" % Test' >> build.sbt

Expand Down
2 changes: 1 addition & 1 deletion examples/java-example-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ The library is cross-compiled for multiple Scala versions:
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>2.12.17</version>
<version>2.12.20</version>
</dependency>
```

Expand Down
2 changes: 1 addition & 1 deletion examples/maven-example-app/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
<profile>
<id>scala-2.12</id>
<properties>
<scala.version>2.12.17</scala.version>
<scala.version>2.12.20</scala.version>
</properties>
<dependencies>
<dependency>
Expand Down
6 changes: 6 additions & 0 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@ addSbtPlugin("com.github.sbt" % "sbt-release" % "1.1.0")

// sbt-ci-release for CI/CD release automation - updated to compatible version
addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12")

// scalafmt for consistent formatting checks
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2")

// Add airframe-log to suppress AirframeLogManager warning from sbt-ci-release
libraryDependencies += "org.wvlet.airframe" %% "airframe-log" % "24.5.0"
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.openbankproject.resourcedocs.core.enforce

import com.openbankproject.resourcedocs.core.model.{RequiredRole, RequestContext, RoleInfoJson}
import com.openbankproject.resourcedocs.core.registry.ResourceDocRegistry

/** Performs access checks against registered ResourceDocs by operationId.
*/
object AccessChecker {

/** Convert role descriptors to RequiredRole for authorization checks. The semantics follow OBP-API: providing
* multiple roles means logical OR.
*/
private def rolesToRequiredRole(roles: Option[List[RoleInfoJson]]): RequiredRole = {
roles match {
case None => RequiredRole.public
case Some(Nil) => RequiredRole.public
case Some(roleInfos) =>
val identifiers = roleInfos.map(_.role).toSet
if (identifiers.isEmpty) RequiredRole.public else RequiredRole.AnyOf(identifiers)
}
}

def require(operationId: String, ctx: RequestContext): Either[String, Unit] = {
ResourceDocRegistry.get(operationId) match {
case None => Left(s"OperationNotRegistered: $operationId")
case Some(docJson) =>
val requiredRole = rolesToRequiredRole(docJson.roles)
if (isAuthorized(requiredRole, ctx.userRoles)) Right(())
else Left(s"InsufficientRoles: $operationId")
}
}

private def isAuthorized(required: RequiredRole, userRoles: Set[String]): Boolean =
required.isAuthorized(userRoles)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.openbankproject.resourcedocs.core.model

/** Represents a named API role.
*/
final case class ApiRole(name: String, description: Option[String] = None)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.openbankproject.resourcedocs.core.model

/** Error documentation entry for an endpoint.
*/
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,20 @@
package com.openbankproject.resourcedocs.core.model

/** HTTP method enumeration independent of any web framework.
*/
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" }

val all: Set[HttpMethod] = Set(GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)

def fromString(method: String): Option[HttpMethod] =
all.find(_.name.equalsIgnoreCase(method))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.openbankproject.resourcedocs.core.model

/** Minimal request context required for access checks.
*/
final case class RequestContext(
userRoles: Set[String],
attributes: Map[String, String] = Map.empty
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.openbankproject.resourcedocs.core.model

/** Expresses required roles for accessing an endpoint.
*/
sealed trait RequiredRole extends Product with Serializable {
def isAuthorized(userRoles: Set[String]): Boolean
}

object RequiredRole {
case object Public extends RequiredRole {
override def isAuthorized(userRoles: Set[String]): Boolean = true
override def toString: String = "Public"
}

final case class AnyOf(roles: Set[String]) extends RequiredRole {
override def isAuthorized(userRoles: Set[String]): Boolean =
roles.isEmpty || roles.exists(userRoles.contains)
}

final case class AllOf(roles: Set[String]) extends RequiredRole {
override def isAuthorized(userRoles: Set[String]): Boolean =
roles.forall(userRoles.contains)
}

/** Disjunction of conjunctions; represents (A & B) | (C) ...
*/
final case class OrOfAnds(groups: Seq[Set[String]]) extends RequiredRole {
override def isAuthorized(userRoles: Set[String]): Boolean =
groups.exists(group => group.forall(userRoles.contains))
}

def anyOf(first: String, rest: String*): RequiredRole = AnyOf((first +: rest).toSet)
def allOf(first: String, rest: String*): RequiredRole = AllOf((first +: rest).toSet)
def oneOfAllOf(firstGroup: Set[String], otherGroups: Set[String]*): RequiredRole = OrOfAnds(firstGroup +: otherGroups)
val public: RequiredRole = Public
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.openbankproject.resourcedocs.core.model

import io.circe.{Json => CirceJson}
import com.github.andyglow.jsonschema.AsCirce._
import json._
import json.schema.Version.Draft04

/** JSON-friendly DTO for API resource documentation. This structure avoids framework types and uses primitive-friendly
* fields.
*/
final case class RoleInfoJson(
role: String,
requires_bank_id: Boolean = false
)

/** Represents where an API call is implemented.
*/
final case class ImplementedByJson(
version: String, // Short hand for the version e.g. "OBPv3.0.0" means Implementations3_0_0
function: String // The val / partial function that implements the call e.g. "getBanks"
)

object OBPResourceDocJson {
/** Helper to generate Schema JSON from a Case Class type.
*
* @tparam T
* The case class type to generate schema for
* @return
* Circe JSON representing the Draft-04 JSON Schema
*/
def generateSchema[T: Schema]: CirceJson = {
// Instantiate the final class directly
val v = new json.schema.Version.Draft04()
Json.schema[T].asCirce(v)
}
}

/** Extended JSON-friendly DTO for API resource documentation that matches OBP-API structure. This structure includes
* all fields from OBP-API's ResourceDocJson for compatibility. Field names use snake_case to match OBP-API's
* ResourceDocJson structure.
*/
final case class OBPResourceDocJson(
operation_id: String,
implemented_by: ImplementedByJson,
request_verb: String,
request_url: String,
summary: String,
description: String, // HTML format
description_markdown: String, // Markdown format
example_request_body: Option[CirceJson] = None,
success_response_body: Option[CirceJson] = None,

error_response_bodies: List[String] = List.empty,
tags: List[String] = List.empty,

// Schema for request body (Draft-04 JSON Schema)
typed_request_body: Option[CirceJson] = None,
// Schema for success response (Draft-04 JSON Schema)
typed_success_response_body: Option[CirceJson] = None,

roles: Option[List[RoleInfoJson]] = None,
is_featured: Boolean = false,
special_instructions: String = "",
specified_url: String,
connector_methods: List[String] = List.empty,
created_by_bank_id: Option[String] = None
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.openbankproject.resourcedocs.core.model

/** Simple tag value object.
*/
final case class Tag(value: String) extends AnyVal
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.openbankproject.resourcedocs.core.registry

import com.openbankproject.resourcedocs.core.model.OBPResourceDocJson

object ConsistencyCheck {
def findDuplicateOperationIds(docs: Seq[OBPResourceDocJson]): Map[String, Int] =
docs
.groupBy(_.operation_id)
.iterator
.map { case (operationId, entries) => operationId -> entries.size }
.filter(_._2 > 1)
.toMap
}
Loading
Loading