Skip to content

Commit

Permalink
APIS-6465: Expose parts of published definitions when publishing (#99)
Browse files Browse the repository at this point in the history
* APIS-6465: Expose parts of published definitions when publishing

* APIS-6465: Use a map instead of includeDefinition
  • Loading branch information
johnsgp authored Oct 26, 2023
1 parent 209f79c commit f92bde3
Show file tree
Hide file tree
Showing 13 changed files with 189 additions and 68 deletions.
66 changes: 45 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,46 +19,70 @@ then publish its definition to the API Definition service and its scope to the A
### POST /publish
Jenkins uses this endpoint to notify of a new microservice
### Request Payload Example
```
```json
{
"serviceName":"hello-world",
"serviceUrl":"http://hello-world.example.com",
"metadata":{
"key1": "value1",
"key2": "value2"
}
"serviceName": "hello-world",
"serviceUrl": "http://hello-world.example.com",
"metadata": {
"key1": "value1",
"key2": "value2"
}
}
```

### Responses
#### 200 OK
The request was successful and the API Definition and Scopes have been published
##### Response Payload Example
```json
{
"name": "Hello World",
"serviceName": "hello-world",
"context": "test/hello",
"description": "A 'hello world' example API",
"versions": [
{
"version": "1.0",
"status": "STABLE",
"endpointsEnabled": true
},
{
"version": "2.0",
"status": "ALPHA",
"endpointsEnabled": false
}
]
}
```
Possible statuses: ALPHA, BETA, STABLE, DEPRECATED, RETIRED

#### 202 Accepted
No response as the request was successful and the API has been published and is awaiting approval
#### 204 No Content
No response as the request was successful and the API Definition and Scopes have been published
No response as the request was successful but the API is awaiting approval in Gatekeeper and has not been published

#### 400 Bad Request
The response will contain information regarding why the request could not be understood
```
```json
{
"statusCode": 400,
"message": "Invalid Json: No content to map due to end-of-input\n at [Source: (akka.util.ByteIterator$ByteArrayIterator$$anon$1); line: 1, column: 0]"
}
```
#### 401 Unauthorized
```
```json
{
"code": "UNAUTHORIZED",
"message": "Agent must be authorised to perform Publish or Validate actions"
}
```
#### 415 Unsupported Media Type
```
```json
{
"statusCode": 415,
"message": "Expecting text/json or application/json body"
}
```
#### 422 Unprocessable Entity - Invalid Request Payload
```
```json
{
"code": "API_PUBLISHER_INVALID_REQUEST_PAYLOAD",
"message": {
Expand All @@ -74,7 +98,7 @@ The response will contain information regarding why the request could not be und
}
```
#### 500 Internal Server Error
```
```json
{
"code": "API_PUBLISHER_UNKNOWN_ERROR",
"message": "An unexpected error occurred: GET of 'http://localhost/api/definition' failed. Caused by: 'Connection refused: localhost/127.0.0.1:80'"
Expand All @@ -83,7 +107,7 @@ The response will contain information regarding why the request could not be und

### POST /validate
### Request Payload Example
```
```json
{
"api": {
"name":"Exmaple API",
Expand Down Expand Up @@ -127,36 +151,36 @@ The response will contain information regarding why the request could not be und
#### 204 No Content
No response as the request was successful
#### 400 Bad Request - Missing Payload
```
```json
{
"statusCode": 400,
"message": "Invalid Json: No content to map due to end-of-input\n at [Source: (akka.util.ByteIterator$ByteArrayIterator$$anon$1); line: 1, column: 0]"
}
```
#### 400 Bad Request - Scope Changed Error
This response is related to the inability to change scopes when publishing.
```
```json
{
"scopeChangedErrors": "Updating scopes while publishing is no longer supported. See https://confluence.tools.tax.service.gov.uk/display/TEC/2021/09/07/Changes+to+scopes for more information"
}
```
#### 400 Bad Request - API Publisher Unknown Error
The message in this response could contain any number of errors related to problems in the request payload. An example is shown below.
```
```json
{
"code": "API_PUBLISHER_UNKNOWN_ERROR",
"message": "An unexpected error occurred: POST of 'http://localhost:9604/api-definition/validate' returned 422. Response body: '{\"code\":\"INVALID_REQUEST_PAYLOAD\",\"messages\":[\"Field 'categories' should exist and not be empty for API 'Exmaple API'\"]}'"
}
```
#### 401 Unauthorized
```
```json
{
"code": "UNAUTHORIZED",
"message": "Agent must be authorised to perform Publish or Validate actions"
}
```
#### 415 Unsupported Media Type
```
```json
{
"statusCode": 415,
"message": "Expecting text/json or application/json body"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ package uk.gov.hmrc.apipublisher.connectors
import java.io.{FileNotFoundException, InputStream}
import java.nio.charset.StandardCharsets.UTF_8
import javax.inject.{Inject, Singleton}
import scala.jdk.CollectionConverters._
import scala.concurrent.duration.FiniteDuration
import scala.concurrent.{ExecutionContext, Future, blocking}
import scala.jdk.CollectionConverters._
import scala.util.Try

import akka.actor.ActorSystem
Expand Down
17 changes: 10 additions & 7 deletions app/uk/gov/hmrc/apipublisher/controllers/PublisherController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import uk.gov.hmrc.http.{HeaderCarrier, UnprocessableEntityException}
import uk.gov.hmrc.play.bootstrap.backend.controller.BackendController

import uk.gov.hmrc.apipublisher.exceptions.UnknownApiServiceException
import uk.gov.hmrc.apipublisher.models.{ApiAndScopes, ErrorCode, ServiceLocation}
import uk.gov.hmrc.apipublisher.models.{ApiAndScopes, ErrorCode, PublicationResult, ServiceLocation}
import uk.gov.hmrc.apipublisher.services.{ApprovalService, DefinitionService, PublisherService}
import uk.gov.hmrc.apipublisher.util.ApplicationLogger
import uk.gov.hmrc.apipublisher.wiring.AppContext
Expand Down Expand Up @@ -78,10 +78,10 @@ class PublisherController @Inject() (

def publish(apiAndScopes: ApiAndScopes): Future[Result] = {
publisherService.publishAPIDefinitionAndScopes(serviceLocation, apiAndScopes).map {
case true =>
case PublicationResult(true, publisherResponse) =>
logger.info(s"Successfully published API Definition and Scopes for ${serviceLocation.serviceName}")
NoContent
case false =>
Ok(Json.toJson(publisherResponse))
case PublicationResult(false, _) =>
logger.info(s"Publication awaiting approval for ${serviceLocation.serviceName}")
Accepted
}
Expand Down Expand Up @@ -116,12 +116,15 @@ class PublisherController @Inject() (
}

def approve(serviceName: String): Action[AnyContent] = Action.async { implicit request =>
({
{
for {
serviceLocation <- approvalService.approveService(serviceName)
result <- publishService(serviceLocation)
result <- publishService(serviceLocation).map {
case Result(ResponseHeader(OK, _, _), _, _, _, _) => NoContent
case other => other
}
} yield result
}) recover recovery(FAILED_TO_APPROVE_SERVICES)
} recover recovery(FAILED_TO_APPROVE_SERVICES)
}

private def handleRequest[T](prefix: String)(f: T => Future[Result])(implicit request: Request[JsValue], reads: Reads[T]): Future[Result] = {
Expand Down
60 changes: 60 additions & 0 deletions app/uk/gov/hmrc/apipublisher/models/PublisherResponse.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2023 HM Revenue & Customs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package uk.gov.hmrc.apipublisher.models

import play.api.libs.json._

case class PublicationResult(approved: Boolean, publisherResponse: Option[PublisherResponse])

case class PublisherResponse(name: String, serviceName: String, context: String, description: String, versions: List[PartialApiVersion])

object PublisherResponse {
implicit val format: OFormat[PublisherResponse] = Json.format[PublisherResponse]
}

case class PartialApiVersion(version: String, status: ApiStatus, endpointsEnabled: Option[Boolean])

object PartialApiVersion {
implicit val format: OFormat[PartialApiVersion] = Json.format[PartialApiVersion]
}

sealed trait ApiStatus

object ApiStatus {
case object ALPHA extends ApiStatus
case object BETA extends ApiStatus
case object STABLE extends ApiStatus
case object DEPRECATED extends ApiStatus
case object RETIRED extends ApiStatus

def apply(text: String): Option[ApiStatus] = text.toUpperCase() match {
case "ALPHA" => Some(ALPHA)
case "BETA" => Some(BETA)
case "STABLE" => Some(STABLE)
case "DEPRECATED" => Some(DEPRECATED)
case "RETIRED" => Some(RETIRED)
case _ => None
}

private val convert: String => JsResult[ApiStatus] = s => ApiStatus(s).fold[JsResult[ApiStatus]](JsError(s"$s is not a status"))(status => JsSuccess(status))

implicit val reads: Reads[ApiStatus] = JsPath.read[String].flatMapResult(convert(_))

implicit val writes: Writes[ApiStatus] = Writes[ApiStatus](status => JsString(status.toString))

implicit val format: Format[ApiStatus] = Format(reads, writes)
}
2 changes: 1 addition & 1 deletion app/uk/gov/hmrc/apipublisher/models/oas/SOpenAPI.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
package uk.gov.hmrc.apipublisher.models.oas

import java.util.function.BiConsumer
import scala.jdk.CollectionConverters._
import scala.collection.immutable.ListMap
import scala.collection.mutable.Buffer
import scala.jdk.CollectionConverters._

import io.swagger.models.Method
import io.swagger.v3.oas.models._
Expand Down
17 changes: 9 additions & 8 deletions app/uk/gov/hmrc/apipublisher/services/PublisherService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class PublisherService @Inject() (
)(implicit val ec: ExecutionContext
) extends ApplicationLogger {

def publishAPIDefinitionAndScopes(serviceLocation: ServiceLocation, apiAndScopes: ApiAndScopes)(implicit hc: HeaderCarrier): Future[Boolean] = {
def publishAPIDefinitionAndScopes(serviceLocation: ServiceLocation, apiAndScopes: ApiAndScopes)(implicit hc: HeaderCarrier): Future[PublicationResult] = {

def apiDetailsWithServiceLocation(apiAndScopes: ApiAndScopes): JsObject = {
apiAndScopes.apiWithoutFieldDefinitions ++ Json.obj(
Expand All @@ -45,12 +45,13 @@ class PublisherService @Inject() (
)
}

def publish(apiAndScopes: ApiAndScopes): Future[Boolean] = {
def publish(apiAndScopes: ApiAndScopes): Future[PublicationResult] = {
for {
_ <- apiScopeConnector.publishScopes(apiAndScopes.scopes)
_ <- apiDefinitionConnector.publishAPI(apiDetailsWithServiceLocation(apiAndScopes))
_ <- publishFieldDefinitions(apiAndScopes.fieldDefinitions)
} yield true
_ <- apiScopeConnector.publishScopes(apiAndScopes.scopes)
api = apiDetailsWithServiceLocation(apiAndScopes)
_ <- apiDefinitionConnector.publishAPI(api)
_ <- publishFieldDefinitions(apiAndScopes.fieldDefinitions)
} yield PublicationResult(approved = true, Some(api.as[PublisherResponse]))
}

def publishFieldDefinitions(fieldDefinitions: Seq[ApiFieldDefinitions]): Future[Unit] = {
Expand All @@ -61,10 +62,10 @@ class PublisherService @Inject() (
}
}

def checkApprovedAndPublish(apiAndScopes: ApiAndScopes): Future[Boolean] = {
def checkApprovedAndPublish(apiAndScopes: ApiAndScopes): Future[PublicationResult] = {
for {
isApproved <- checkApproval(serviceLocation, apiAndScopes.apiName, apiAndScopes.description)
result <- if (isApproved) publish(apiAndScopes) else successful(false)
result <- if (isApproved) publish(apiAndScopes) else successful(PublicationResult(approved = false, None))
} yield result
}

Expand Down
14 changes: 7 additions & 7 deletions it/uk/gov/hmrc/apipublisher/PublisherFeatureSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ class PublisherFeatureSpec extends BaseFeatureSpec {
| "versions": [
| {
| "version": "1.0",
| "status": "PUBLISHED"
| "status": "STABLE"
| }
| ]
| }
Expand All @@ -184,7 +184,7 @@ class PublisherFeatureSpec extends BaseFeatureSpec {
| "versions": [
| {
| "version": "1.0",
| "status": "PUBLISHED",
| "status": "STABLE",
| "fieldDefinitions": [
| {
| "name": "callbackUrl",
Expand All @@ -202,11 +202,11 @@ class PublisherFeatureSpec extends BaseFeatureSpec {
| },
| {
| "version": "2.0",
| "status": "PUBLISHED"
| "status": "STABLE"
| },
| {
| "version": "3.0",
| "status": "PUBLISHED",
| "status": "STABLE",
| "fieldDefinitions": [
| {
| "name": "callbackUrlOnly",
Expand Down Expand Up @@ -266,7 +266,7 @@ class PublisherFeatureSpec extends BaseFeatureSpec {
| "versions" : [
| {
| "version" : "1.0",
| "status" : "PUBLISHED",
| "status" : "STABLE",
| "endpoints": [
| {
| "uriPattern": "/hello",
Expand All @@ -279,7 +279,7 @@ class PublisherFeatureSpec extends BaseFeatureSpec {
| },
| {
| "version" : "2.0",
| "status" : "PUBLISHED",
| "status" : "STABLE",
| "endpoints": [
| {
| "uriPattern": "/hello",
Expand All @@ -293,7 +293,7 @@ class PublisherFeatureSpec extends BaseFeatureSpec {
| },
| {
| "version" : "3.0",
| "status" : "PUBLISHED",
| "status" : "STABLE",
| "endpoints": [
| {
| "uriPattern": "/hello",
Expand Down
2 changes: 1 addition & 1 deletion run_local_with_dependencies.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/bin/bash

sm --start API_DEFINITION API_SCOPE API_SUBSCRIPTION_FIELDS API_EXAMPLE_MICROSERVICE API_DOCUMENTATION_FRONTEND
sm2 --start API_DEFINITION API_SCOPE API_SUBSCRIPTION_FIELDS API_EXAMPLE_MICROSERVICE API_DOCUMENTATION_FRONTEND

./run_local.sh
Loading

0 comments on commit f92bde3

Please sign in to comment.