Skip to content
This repository has been archived by the owner on Dec 22, 2022. It is now read-only.

Feature/variable endpoints #12

Merged
merged 13 commits into from
Feb 3, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
153 changes: 142 additions & 11 deletions app/controllers/RestController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,126 @@ import javax.inject.Inject

import controllers.conversion._
import controllers.conversion.Converter._
import org.scalarules.engine.Context
import org.scalarules.engine.{Context, FactEngine}
import org.scalarules.facts.Fact
import play.api.libs.json.{JsError, JsObject, JsSuccess, JsValue}
import play.api.mvc.{Action, Controller, Request}
import services.{DerivationsService, GlossariesService, JsonConversionMapsService}
import org.scalarules.service.dsl.BusinessService
import play.api.data.validation.ValidationError
import play.api.libs.json._
import play.api.mvc.{Action, Controller, Request, Result}
import services.{BusinessServicesService, DerivationsService, GlossariesService, JsonConversionMapsService}

import scala.util.{Failure, Success, Try}

// scalastyle:off public.methods.have.type

class RestController @Inject() (derivationsService: DerivationsService, glossariesService: GlossariesService, jsonConversionMapsService: JsonConversionMapsService) extends Controller {
class RestController @Inject() (businessServicesService: BusinessServicesService,
derivationsService: DerivationsService,
glossariesService: GlossariesService,
jsonConversionMapsService: JsonConversionMapsService) extends Controller {

val endpoints: Try[JsObject] = businessServicesService.businessServiceNames.map(
businessServiceName => JsObject(Map(("/api/run/group/" + businessServiceName) -> Json.toJson("/api/run/group/information/" + businessServiceName)))
) match {
case Nil => Failure(new IllegalStateException("No endpoints available: it seems no BusinessServices have been defined!"))
case jsObjectList: List[JsObject] => Success(jsObjectList.reduceLeft(_ ++ _))
}

/**
* @return a list of JsObjects where the first value is the endpoint and the second value is the information endpoint for all available BusinessServices
* or an InternalServerError(500) if no BusinessServices have been found as this suggests a configuration error.
*/
def businessservices = Action(
endpoints match {
case f: Failure[JsObject] => InternalServerError(f.exception.getMessage)
case s: Success[JsObject] => Ok(s.value)
}
)

/**
* provides information on verplichteInvoer, uitvoer and optioneleFacts
* @param name: the name of the BusinessService for which
* @return
*/
def information(name: String) = Action {
findBusinessService(name) match {
case f: Failure[(String, BusinessService)] => BadRequest(JsError.toJson(JsError(ValidationError(f.exception.getMessage))))
case s: Success[(String, BusinessService)] => Ok(
JsObject(Map(
"Information for Business Service " + s.value._1 ->
JsObject(Map(
"verplichteInvoer" -> contextToJson(s.value._2.verplichteInvoerFacts.map(f => f -> ("type " + f.valueType)).toMap, jsonConversionMap),
"optioneleInvoer met bijbehorende defaults" -> contextToJson(s.value._2.optioneleInvoerFacts, jsonConversionMap),
"uitvoer" -> contextToJson(s.value._2.uitvoerFacts.map(f => f -> ("type " + f.valueType)).toMap, jsonConversionMap)))
)))
}
}

/**
* Attempts to run the derivations specified by the named BusinessService with the JSON context provided.
* Will provide clear error information on all detected issues. Otherwise will provide the provided inputs and the outputs.
* @param name: the name of the BusinessService whose derivations are meant to be run, currently case sensitive
* @return the provided inputs and the specified outputs, nicely sorted.
*/
def runBusinessService(name: String) = Action(parse.json) {
request =>
findBusinessService(name) match {
case f: Failure[(String, BusinessService)] => BadRequest(JsError.toJson(JsError(ValidationError(f.exception.getMessage))))
case s: Success[(String, BusinessService)] => runBusiness(request, InputsAndOutputsResponseJsObject, s.value._2)
}
}

/**
* Attempts to run the derivations specified by the named BusinessService with the JSON context provided.
* Will provide clear error information on all detected issues. Otherwise will provide the provided context, all intermediary results and the outputs.
* @param name: the name of the BusinessService whose derivations are meant to be run, currently case sensitive
* @return The inputs, intermediary results and outputs, nicely sorted.
*/
def debugBusinessService(name: String) = Action(parse.json) {
request =>
findBusinessService(name) match {
case f: Failure[(String, BusinessService)] => BadRequest(JsError.toJson(JsError(ValidationError(f.exception.getMessage))))
case s: Success[(String, BusinessService)] => runBusiness(request, CompleteResponseJsObject, s.value._2)
}
}

/**
* Attempts to run the derivations specified by the named BusinessService with the JSON context provided.
* Will provide clear error information on all detected issues. Otherwise will provide only the specified uitvoer.
* @param name: the name of the BusinessService whose derivations are meant to be run, currently case sensitive
* @return only the outputs belonging to the BusinessService
*/
def runBusinessServiceOutputsOnly(name: String) = Action(parse.json) {
request =>
findBusinessService(name) match {
case f: Failure[(String, BusinessService)] => BadRequest(JsError.toJson(JsError(ValidationError(f.exception.getMessage))))
case s: Success[(String, BusinessService)] => runBusiness(request, OutputsOnlyResponseJsObject, s.value._2)
}
}


private def findBusinessService(name: String): Try[(String, BusinessService)] = {
val matchedBusinessServices = businessServicesService.businessServices.collect{ case (naam, service) if naam == name => (naam, service)}
matchedBusinessServices match {
case Nil => Failure(
new IllegalArgumentException("No BusinessService matched this name, make sure you have used the proper endpoint definition!" ++ businessServicesService.businessServiceNames.toString)
)
case head :: tail => tail match {
case Nil => Success(head)
case tail: List[(String, BusinessService)] => Failure(
new IllegalStateException("More than one BusinessService matched this name. Suspected mistake in BusinessService specifications.")
)
}
}
}

private def runBusiness(request: Request[JsValue], jsonResponseProvider: ResponseJsObject, businessService: BusinessService): Result = {
val (initialContextFragments: List[JsSuccess[Context]], conversionErrors: List[JsError]) = {
convertToIndividualContext(request.body, businessService.glossaries.foldLeft(Map.empty[String, Fact[Any]])((acc, glossary) => acc ++ glossary.facts), jsonConversionMap)
}

if (conversionErrors != List.empty) BadRequest( processConversionErrors(conversionErrors) )
else processConvertedContextBusinessService(initialContextFragments, jsonResponseProvider, businessService)
}

/**
* Provides a REST endpoint for triggering all derivations in the target project. Any fact definitions available in the target project's glossaries
Expand All @@ -29,7 +140,7 @@ class RestController @Inject() (derivationsService: DerivationsService, glossari
* - A JsObject containing one JsObject: "facts", which contains the combined information of "input" and "results"
*/
def runAll = Action(parse.json) { request =>
run(request, DefaultResponseJsObject)
run(request, RunAllResponseJsObject)
}

/**
Expand All @@ -40,7 +151,7 @@ class RestController @Inject() (derivationsService: DerivationsService, glossari
* - A JsObject containing two JsObject: "input" and "results", which contains only the information of "results"
*/
def runAllDebug = Action(parse.json) { request =>
run(request, DebugResponseJsObject)
run(request, DebugAllResponseJsObject)
}

/**
Expand All @@ -51,7 +162,7 @@ class RestController @Inject() (derivationsService: DerivationsService, glossari
* - A JsObject containing one JsObject: "results", which contains only the information of "results"
*/
def runAllResultsOnly = Action(parse.json) { request =>
run(request, ResultsOnlyResponseJsObject)
run(request, RunAllResultsOnlyResponseJsObject)
}


Expand All @@ -62,16 +173,36 @@ class RestController @Inject() (derivationsService: DerivationsService, glossari
convertToIndividualContext(request.body, glossariesService.mergedGlossaries, jsonConversionMap)

if (conversionErrors != List.empty) BadRequest( processConversionErrors(conversionErrors) )
else Ok( processConvertedContext(initialContextFragments, jsonResponseProvider) )
else Ok( processConvertedContext(initialContextFragments, Nil, jsonResponseProvider) )
}



private def processConversionErrors(conversionErrors: List[JsError]): JsObject = JsError.toJson(conversionErrors.reduceLeft(_ ++ _))

private def processConvertedContext(initialContextFragments: List[JsSuccess[Context]], jsonResponse: ResponseJsObject): JsObject = {
private def processConvertedContext(initialContextFragments: List[JsSuccess[Context]], uitvoerFacts: List[Fact[Any]], jsonResponse: ResponseJsObject): JsObject = {
val initialContext: Context = initialContextFragments.foldLeft(Map.empty[Fact[Any], Any])((acc, jsSuccess) => acc ++ jsSuccess.get)
val resultContext: Context = RulesRunner.run(initialContext, derivationsService.topLevelDerivations)

jsonResponse.toJson(initialContext = initialContext, resultContext = resultContext, jsonConversionMap)
jsonResponse.toJson(initialContext = initialContext, uitvoerFacts = uitvoerFacts, resultContext = resultContext, jsonConversionMap)
}

private def processConvertedContextBusinessService(initialContextFragments: List[JsSuccess[Context]], jsonResponse: ResponseJsObject, businessService: BusinessService): Result = {
val initialContext: Context = initialContextFragments.foldLeft(Map.empty[Fact[Any], Any])((acc, jsSuccess) => acc ++ jsSuccess.get)
val resultContext: Try[Context] = businessService.run(initialContext, FactEngine.runNormalDerivations)

resultContext match {
case f: Failure[Context] => BadRequest( JsError.toJson(JsError(ValidationError("Attempt at calculation failed due to validation errors: " + f.exception.getMessage))) )
case s: Success[Context] => Ok(
jsonResponse.toJson(
initialContext = initialContext,
uitvoerFacts = businessService.uitvoerFacts,
resultContext = s.value,
jsonConversionMap
)
)
}

}

}
95 changes: 56 additions & 39 deletions app/controllers/conversion/Conversion.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,59 +4,61 @@ import controllers.conversion.ImplicitConversions._
import org.scalarules.facts.Fact
import org.scalarules.finance.nl._
import play.api.data.validation.ValidationError
import play.api.libs.json._
import play.api.libs.json.{JsObject, _}

import scala.reflect.{ClassTag, classTag}
import scala.reflect.runtime.universe._

trait JsonConversionsProvider {
def contextToJsonConversions: Map[Class[_], (Fact[Any], Any) => JsObject]
def jsonToFactConversions: Map[String, ConvertToFunc]
}

object DefaultJsonConversion extends JsonConversionsProvider {
override def contextToJsonConversions: Map[Class[_], ConvertBackFunc] = ContextToJsonConversionMap.contextToJsonConversionMap
override def jsonToFactConversions: Map[String, ConvertToFunc] = JsonToFactConversionMap.jsonToFactConversionMap

object ContextToJsonConversionMap {
val contextToJsonConversionMap: Map[Class[_], ConvertBackFunc] = Map[Class[_], ConvertBackFunc](
classOf[String] -> { contextStringToJsObject(_, _) },
classOf[Bedrag] -> { contextBedragToJsObject(_, _) },
classOf[Percentage] -> { contextPercentageToJsObject(_, _) },
classOf[BigDecimal] -> { contextBigDecimalToJsObject(_, _) },
classOf[Boolean] -> { contextBooleanToJsObject(_, _) },
classOf[java.lang.Boolean] -> { contextBooleanToJsObject(_, _) }
)
private def convertFacts(fact: Fact[Any], factValue: Any): JsObject = JsObject(Map(fact.name -> turnFactsIntoJson(factValue)))

private def contextStringToJsObject(fact: Fact[Any], factValue: Any): JsObject = factValue match {
case string: String => JsObject(Map(fact.name -> Json.toJson(factValue.toString)))
case _ => throw new IllegalArgumentException
}
private def matchMap[A : ClassTag, B: ClassTag](map: Map[A, B]) = map match {
case context: Map[Fact[Any] @unchecked, Any @unchecked] if classTag[A] == classTag[Fact[Any]] =>
context.map{ case (key, value) => convertFacts(key.asInstanceOf[Fact[Any]], value)}.reduceLeft(_ ++ _)
}

private def contextBedragToJsObject(fact: Fact[Any], factValue: Any): JsObject = factValue match {
case bedrag: Bedrag => JsObject(Map(fact.name -> Json.toJson[Bedrag](bedrag)))
case _ => throw new IllegalArgumentException
private def turnFactsIntoJson(factValue: Any): JsValue = //scalastyle:ignore cyclomatic.complexity
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^_^

try { userSpecifiedConversionsToJson(factValue) }
catch { case e: Exception => factValue match {
case map: Map[Any @unchecked, Any @unchecked] => matchMap(map)
case x :: xs => JsArray(for { elem <- (x :: xs)} yield turnFactsIntoJson(elem))
case bedrag: Bedrag => Json.toJson(bedrag)
case string: String => Json.toJson(string)
case bool: Boolean => JsBoolean(bool)
case bool: java.lang.Boolean => JsBoolean(bool)
case bigDecimal: BigDecimal => Json.toJson(bigDecimal)
case percentage: Percentage => Json.toJson(percentage)
case other: Any => throw new IllegalStateException(s"No legal conversion found for $other, with type ${other.getClass} " + e.fillInStackTrace())
}

private def contextPercentageToJsObject(fact: Fact[Any], factValue: Any): JsObject = factValue match {
case percentage: Percentage => JsObject(Map(fact.name -> Json.toJson[Percentage](percentage)))
case _ => throw new IllegalArgumentException
}

private def contextBigDecimalToJsObject(fact: Fact[Any], factValue: Any): JsObject = factValue match {
case bigDecimal: BigDecimal => JsObject(Map(fact.name -> Json.toJson[BigDecimal](bigDecimal)))
case _ => throw new IllegalArgumentException
}
def contextToJsonConversions(fact: Fact[Any], factValue: Any): JsObject = convertFacts(fact, factValue)

/**
* override this method in your JsonConversionsProvider to add conversions to Json for types you have implemented,
* without losing all the predefined conversions (custom-specified takes precendence).
* Example:
* override def userSpecifiedConversionsToJson(factValue: Any): JsValue = factValue match {
case thingOfYourType: YourType => Json.toJson[YourType](thingOfYourType)
}
* @param factValue
* @return
*/
def userSpecifiedConversionsToJson(factValue: Any): JsValue = factValue match {
case _ => throw new IllegalStateException("None of the default matches succeeded and no other matches were provided")
}

private def contextBooleanToJsObject(fact: Fact[Any], factValue: Any): JsObject = factValue match {
case bool: Boolean => JsObject(Map(fact.name -> JsBoolean(bool)))
case bool: java.lang.Boolean => JsObject(Map(fact.name -> JsBoolean(bool)))
case _ => throw new IllegalArgumentException
}
def jsonToFactConversions: Map[String, ConvertToFunc]
}

}
object DefaultJsonConversion extends JsonConversionsProvider {
override def jsonToFactConversions: Map[String, ConvertToFunc] = JsonToFactConversionMap.jsonToFactConversionMap

object JsonToFactConversionMap {
val jsonToFactConversionMap: Map[String, ConvertToFunc] = Map[String, ConvertToFunc](
weakTypeOf[List[List[List[Bedrag]]]].toString.replace("scala.", "") -> { bedragLijstLijstLijstFunct(_, _) },
weakTypeOf[List[List[Bedrag]]].toString.replace("scala.", "") -> { bedragLijstLijstFunct(_, _) },
weakTypeOf[List[Bedrag]].toString.replace("scala.", "") -> { bedragLijstFunct(_, _) },
classOf[String].getTypeName -> { stringFunct(_, _) },
weakTypeOf[String].toString -> { stringFunct(_, _) },
classOf[Bedrag].getTypeName -> { bedragFunct(_, _) },
Expand All @@ -77,6 +79,21 @@ object DefaultJsonConversion extends JsonConversionsProvider {
case _ => JsError(ValidationError(s"Conversion for BigDecimal fact ${fact.name} failed, corresponding value was not of expected type JsNumber"))
}

private def bedragLijstLijstLijstFunct(fact: Fact[Any], factValue: JsValue): JsResult[List[List[List[Bedrag]]]] = factValue match {
case jsNumber: JsArray => Json.fromJson[List[List[List[Bedrag]]]](jsNumber)
case _ => JsError(ValidationError(s"Conversion for Bedrag fact ${fact.name} failed, corresponding value was not of expected type JsNumber"))
}

private def bedragLijstLijstFunct(fact: Fact[Any], factValue: JsValue): JsResult[List[List[Bedrag]]] = factValue match {
case jsNumber: JsArray => Json.fromJson[List[List[Bedrag]]](jsNumber)
case _ => JsError(ValidationError(s"Conversion for Bedrag fact ${fact.name} failed, corresponding value was not of expected type JsNumber"))
}

private def bedragLijstFunct(fact: Fact[Any], factValue: JsValue): JsResult[List[Bedrag]] = factValue match {
case jsNumber: JsArray => Json.fromJson[List[Bedrag]](jsNumber)
case _ => JsError(ValidationError(s"Conversion for Bedrag fact ${fact.name} failed, corresponding value was not of expected type JsNumber"))
}

private def bedragFunct(fact: Fact[Any], factValue: JsValue): JsResult[Bedrag] = factValue match {
case jsNumber: JsNumber => Json.fromJson[Bedrag](jsNumber)
case jsString: JsString => Json.fromJson[Bedrag](jsString)
Expand Down
4 changes: 3 additions & 1 deletion app/controllers/conversion/Converter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,7 @@ object Converter {
(successes, errors)
}

def contextToJson(context: Context, jsonConversionMap: JsonConversionsProvider): JsObject = writes(context, jsonConversionMap)
def contextToJson(context: Context, jsonConversionMap: JsonConversionsProvider): JsObject =
if (context.isEmpty) JsObject(Map.empty[String, JsValue])
else writes(context, jsonConversionMap)
}
6 changes: 1 addition & 5 deletions app/controllers/conversion/ImplicitConversions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,7 @@ object ImplicitConversions {
*/
def writes(context: Context, conversionMap: JsonConversionsProvider): JsObject = {
context.map{ case (fact:Fact[Any], factValue: Any) =>
conversionMap.contextToJsonConversions.get(factValue.getClass) match {
case function: Some[ConvertBackFunc] => function.get(fact, factValue)
case None => throw new IllegalStateException(s"Unable to find suitable toJson conversion for Fact with name ${fact.name} with " +
s"valuetype ${factValue.getClass.getTypeName} in factConversionMap")
}
conversionMap.contextToJsonConversions(fact, factValue)
}.reduceLeft(_ ++ _)
}
}
Expand Down