Skip to content

Commit

Permalink
Merge pull request #4 from nextbss/custom-error-responses
Browse files Browse the repository at this point in the history
NF - Custom error responses
  • Loading branch information
AlexJuca committed Sep 8, 2020
2 parents e503c00 + a323a17 commit 98c0893
Show file tree
Hide file tree
Showing 18 changed files with 248 additions and 60 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ build/
### VS Code ###
.vscode/
settings.xml
target
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>

<build>
Expand Down
22 changes: 22 additions & 0 deletions src/main/kotlin/ao/co/nextbss/requirekt/AAACustomErrorViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ao.co.nextbss.requirekt

import co.ao.nextbss.requirekt.annotation.ErrorResponse

@ErrorResponse
class AAACustomErrorViewModel(
var status: Int = 0,
var code: String? = null,
var message: String? = null,
var type: String? = null): AbstractErrorResponse() {

override fun toJSON(vararg args: ArrayList<Any>): String {
status = fromArgsAsInt(0, args)
code = fromArgsAsString(1, args)
message = fromArgsAsString(2, args)
type = fromArgsAsString(3, args)
return super.toJSON()
}
}



Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ao.co.nextbss.requirekt

import co.ao.nextbss.requirekt.interfaces.Converter

abstract class AbstractErrorResponse: Converter {
override fun toJSON(vararg args: ArrayList<Any>): String {
return ErrorWrapper().toJsonString(this)
}
}
2 changes: 1 addition & 1 deletion src/main/kotlin/ao/co/nextbss/requirekt/ApiException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ package ao.co.nextbss.requirekt

import org.springframework.http.HttpStatus

class ApiException(val error: String, val status: HttpStatus = HttpStatus.BAD_REQUEST) : RuntimeException()
class ApiException(val error: String, val status: Int = HttpStatus.BAD_REQUEST.value()) : RuntimeException()
16 changes: 16 additions & 0 deletions src/main/kotlin/ao/co/nextbss/requirekt/CustomErrorBeanFinder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package ao.co.nextbss.requirekt

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.ComponentScan
import org.springframework.core.env.Environment
import org.springframework.stereotype.Component

@Component
@ComponentScan(basePackages = [])
open class CustomErrorBeanFinder {
@Autowired
lateinit var errors: List<AbstractErrorResponse>

@Autowired
lateinit var environment: Environment
}
14 changes: 14 additions & 0 deletions src/main/kotlin/ao/co/nextbss/requirekt/DefaultErrorViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package ao.co.nextbss.requirekt

import ao.co.nextbss.requirekt.AbstractErrorResponse
import co.ao.nextbss.requirekt.annotation.ErrorResponse

@ErrorResponse
class DefaultErrorViewModel(val status: Int?,
val code: String?,
val message: String?
) : AbstractErrorResponse() {
override fun toJSON(vararg args: ArrayList<Any>): String {
return super.toJSON()
}
}
10 changes: 10 additions & 0 deletions src/main/kotlin/ao/co/nextbss/requirekt/ErrorResponse.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package ao.co.nextbss.requirekt

import co.ao.nextbss.requirekt.annotation.ErrorResponse

@ErrorResponse
open class ErrorResponse: AbstractErrorResponse() {
override fun toJSON(vararg args: ArrayList<Any>): String {
return ErrorWrapper().toJsonString(this)
}
}
47 changes: 0 additions & 47 deletions src/main/kotlin/ao/co/nextbss/requirekt/ErrorViewModel.kt

This file was deleted.

7 changes: 5 additions & 2 deletions src/main/kotlin/ao/co/nextbss/requirekt/ErrorWrapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package ao.co.nextbss.requirekt

import co.ao.nextbss.Yoru

internal class ErrorWrapper(val errors: List<ErrorViewModel>) {
fun toJsonString(): String {
open class ErrorWrapper {
val errors = ArrayList<AbstractErrorResponse>()

fun toJsonString(error: AbstractErrorResponse): String {
errors.add(error)
return Yoru<ErrorWrapper>().toJson(this)
}
}
82 changes: 73 additions & 9 deletions src/main/kotlin/ao/co/nextbss/requirekt/Preconditions.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,73 @@
package ao.co.nextbss.requirekt

import org.springframework.beans.BeansException
import org.springframework.context.ApplicationContext
import org.springframework.context.ApplicationContextAware
import org.springframework.core.SpringProperties
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Component
import java.lang.IllegalStateException
import java.util.logging.Logger
import kotlin.collections.ArrayList

/**
* Throws an [ApiException] with the result of calling [lazyMessage] if the [value] is false.
* Note: HttpStatus is [Bad_REQUEST] by default
*
* Note: HttpStatus is [Bad_REQUEST] by default.
*/
inline fun require(value: Boolean, lazyMessage: () -> Any) {
if (!value) {
throw ApiException(ErrorViewModel.singleJSON(HttpStatus.BAD_REQUEST, lazyMessage().toString()))
throw ApiException(
DefaultErrorViewModel(
HttpStatus.BAD_REQUEST.value(),
"",
lazyMessage().toString()
).toJSON())
}
}

/**
* Throws an [ApiException] for a CustomError with the result of [args] if the [value] is false
*/
inline fun require(value: Boolean, vararg args: ArrayList<Any>,) {
val logger: Logger = Logger.getLogger("Preconditions")
if (!value) {
val customErrorBeanFinder = SpringContext.getBean(CustomErrorBeanFinder::class.java)
val errors: List<AbstractErrorResponse> = customErrorBeanFinder.errors
logger.info { "Number of error types found: ${errors.size} " }
errors.forEach { println(it.javaClass.canonicalName) }

val filteredListOfCustomErrors = errors.filterNot {
it.javaClass.canonicalName == "ao.co.nextbss.requirekt.DefaultErrorViewModel"
|| it.javaClass.canonicalName == "ao.co.nextbss.requirekt.ErrorResponse"
|| it.javaClass.canonicalName == "ao.co.nextbss.requirekt.ErrorResponse"
}

logger.info { "Number of filtered error types found: ${filteredListOfCustomErrors.size}" }

filteredListOfCustomErrors.forEach { println(it.javaClass.canonicalName) }

val nameOfCustomClassToInstantiate = if (customErrorBeanFinder.environment.activeProfiles.contains("test")) {
filteredListOfCustomErrors[0].javaClass.canonicalName
} else {
try {
filteredListOfCustomErrors[1].javaClass.canonicalName
} catch (e: IndexOutOfBoundsException) {
throw IllegalStateException("No CustomError added. Add a Custom Error that subclasses AbstractErrorResponse and annotate it with @ErrorResponse")
}
}

logger.info("Canonical name of class that will instantiated as a custom error: $nameOfCustomClassToInstantiate")

val customError: AbstractErrorResponse = Class.forName(nameOfCustomClassToInstantiate)
.getDeclaredConstructor().newInstance() as AbstractErrorResponse

throw ApiException(
customError.toJSON(*args),
fromArgsAsInt(
0,
args
)
)
}
}

Expand All @@ -19,7 +77,13 @@ inline fun require(value: Boolean, lazyMessage: () -> Any) {
*/
inline fun require(value: Boolean, httpStatus: HttpStatus, lazyMessage: () -> Any) {
if (!value) {
throw ApiException(ErrorViewModel.singleJSON(httpStatus, lazyMessage().toString()), httpStatus)
throw ApiException(
DefaultErrorViewModel(
httpStatus.value(),
"",
lazyMessage().toString()
).toJSON(),
httpStatus.value())
}
}

Expand All @@ -29,12 +93,12 @@ inline fun require(value: Boolean, httpStatus: HttpStatus, lazyMessage: () -> An
*/
inline fun require(value: Boolean, status: HttpStatus, errorCode: String, lazyMessage: () -> Any) {
if (!value) {
throw ApiException(ErrorViewModel.singleJSON(
status,
throw ApiException(
DefaultErrorViewModel(
status.value(),
errorCode,
lazyMessage().toString()
),
status = status
)
).toJSON(),
status.value())
}
}
22 changes: 22 additions & 0 deletions src/main/kotlin/ao/co/nextbss/requirekt/SpringContext.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ao.co.nextbss.requirekt

import org.springframework.beans.BeansException
import org.springframework.context.ApplicationContext
import org.springframework.context.ApplicationContextAware
import org.springframework.stereotype.Component

@Component
class SpringContext : ApplicationContextAware {
@Throws(BeansException::class)
override fun setApplicationContext(context: ApplicationContext?) {
Companion.context = context
}

companion object {
private var context: ApplicationContext? = null

fun <T : Any?> getBean(beanClass: Class<T>?): T {
return context!!.getBean(beanClass)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package co.ao.nextbss.requirekt.annotation

import org.springframework.stereotype.Component

@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Component
annotation class ErrorResponse(

)
17 changes: 17 additions & 0 deletions src/main/kotlin/ao/co/nextbss/requirekt/extensions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package ao.co.nextbss.requirekt

fun fromArgs(index: Int, args: Array<out ArrayList<Any>>): Any {
return args[0][index]
}

fun fromArgsAsInt(index: Int, args: Array<out ArrayList<Any>>): Int {
return args[0][index] as Int
}

fun fromArgsAsString(index: Int, args: Array<out ArrayList<Any>>): String {
return args[0][index] as String
}

fun fromArgsAsFloat(index: Int, args: Array<out ArrayList<Any>>): Float {
return args[0][index] as Float
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package co.ao.nextbss.requirekt.interfaces

interface Converter {
fun toJSON(vararg args: ArrayList<Any>): String
}
23 changes: 23 additions & 0 deletions src/test/kotlin/ao/co/nextbss/requirekt/RequireTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,27 @@ internal class RequireTest(
.jsonPath("$.['errors'].[0].['code']")
.value(""))
}

@Test
@DisplayName("It Should Return custom error response in json")
fun itShouldReturnCustomErrorResponse() {
mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/messages/xHrFuz/transfer"))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isForbidden)
.andExpect(
MockMvcResultMatchers
.jsonPath("$.['errors'].[0].['message']")
.value("Access forbidden. You are not allowed to administrate categories."))
.andExpect(
MockMvcResultMatchers
.jsonPath("$.['errors'].[0].['code']")
.value("104"))
.andExpect(
MockMvcResultMatchers
.jsonPath("$.['errors'].[0].['type']")
.value("authentication"))
.andExpect(MockMvcResultMatchers
.jsonPath("$.['errors'].[0].['status']")
.value("403"))
}
}
14 changes: 13 additions & 1 deletion src/test/kotlin/ao/co/nextbss/requirekt/RestApiController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,19 @@ class RestApiController {
require(false) {
"Message is invalid"
}
return ResponseEntity.ok().build()
}

@PostMapping("/xHrFuz/transfer")
fun custom(): ResponseEntity<Any> {
require(value = false,
arrayListOf(
HttpStatus.FORBIDDEN.value(),
"104",
"Access forbidden. You are not allowed to administrate categories.",
"authentication"
)
)
return ResponseEntity.ok().build()
}
}
}
Binary file modified target/classes/META-INF/requirekt.kotlin_module
Binary file not shown.

0 comments on commit 98c0893

Please sign in to comment.