diff --git a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/data/Compliance.scala b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/data/Compliance.scala index 0b282f68aaf..7f32e6d1c66 100644 --- a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/data/Compliance.scala +++ b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/data/Compliance.scala @@ -48,6 +48,7 @@ import com.normation.rudder.domain.reports.ComplianceLevel import com.normation.rudder.reports.ComplianceModeName import com.normation.rudder.repository.FullActiveTechnique import enumeratum.* +import io.scalaland.chimney.Transformer import java.lang import net.liftweb.json.* import net.liftweb.json.JsonAST @@ -55,6 +56,8 @@ import net.liftweb.json.JsonDSL.* import org.apache.commons.csv.CSVFormat import org.apache.commons.csv.QuoteMode import scala.collection.immutable +import zio.json.DeriveJsonEncoder +import zio.json.JsonEncoder /** * Here, we want to present two views of compliance: @@ -133,9 +136,31 @@ final case class ByNodeGroupFullCompliance( id: String, name: String, category: String, - targeted: ByNodeGroupCompliance, - global: ByNodeGroupCompliance + global: GenericCompliance, + targeted: GenericCompliance ) + +final case class GenericCompliance( + id: String, + name: String, + compliance: ComplianceLevel, + mode: ComplianceModeName, + complianceDetails: ComplianceSerializable +) +object GenericCompliance { + implicit def transformByNodeGroupCompliance(implicit + precision: CompliancePrecision + ): Transformer[ByNodeGroupCompliance, GenericCompliance] = { + Transformer + .define[ByNodeGroupCompliance, GenericCompliance] + .withFieldComputed( + _.complianceDetails, + b => ComplianceSerializable.fromPercent(CompliancePercent.fromLevels(b.compliance, precision)) + ) + .buildTransformer + } +} + final case class ByNodeGroupCompliance( id: String, name: String, @@ -505,6 +530,18 @@ object CsvCompliance { } object JsonCompliance { + implicit val complianceModeNameEncoder: JsonEncoder[ComplianceModeName] = JsonEncoder[String].contramap(_.name) + implicit val complianceSerializableEncoder: JsonEncoder[ComplianceSerializable] = DeriveJsonEncoder.gen[ComplianceSerializable] + + class ComplianceEncoders(implicit val precision: CompliancePrecision) { + implicit val complianceLevelEncoder: JsonEncoder[ComplianceLevel] = { + JsonEncoder[Double].contramap(_.complianceWithoutPending(precision)) + } + implicit val genericComplianceEncoder: JsonEncoder[GenericCompliance] = + DeriveJsonEncoder.gen[GenericCompliance] + implicit val byNodeGroupFullComplianceEncoder: JsonEncoder[ByNodeGroupFullCompliance] = + DeriveJsonEncoder.gen[ByNodeGroupFullCompliance] + } // global compliance implicit class JsonGlobalCompliance(val optCompliance: Option[(ComplianceLevel, Long)]) extends AnyVal { diff --git a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/lift/ComplianceApi.scala b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/lift/ComplianceApi.scala index 3a611a94c65..4cc6b6034ba 100644 --- a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/lift/ComplianceApi.scala +++ b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/lift/ComplianceApi.scala @@ -86,6 +86,7 @@ import com.normation.rudder.services.reports.ReportingService import com.normation.rudder.services.reports.ReportingServiceUtils import com.normation.rudder.web.services.ComputePolicyMode import com.normation.zio.currentTimeMillis +import io.scalaland.chimney.syntax.* import net.liftweb.common.* import net.liftweb.http.LiftResponse import net.liftweb.http.PlainTextResponse @@ -100,6 +101,7 @@ import zio.syntax.* class ComplianceApi( restExtractorService: RestExtractorService, + zioJsonExtractor: ZioJsonExtractor, complianceService: ComplianceAPIService, readDirective: RoDirectiveRepository ) extends LiftApiModuleProvider[API] { @@ -333,13 +335,17 @@ class ComplianceApi( ): LiftResponse = { implicit val qc: QueryContext = authzToken.qc - (for { - precision <- restExtractor.extractCompliancePrecisionFromParams(req.params).toIO - filters = QueryFilter(req) - group <- complianceService.getNodeGroupComplianceSummary(filters, precision) - } yield { - group - }).chainError("Could not get compliance summary").toLiftResponseOne(params, schema, _ => None) + withEncodersCtx( + restExtractor.extractCompliancePrecisionFromParams(req.params).map(_.getOrElse(CompliancePrecision.Level2)), + params, + schema + ) { encoders => + import encoders.* + complianceService + .getNodeGroupComplianceSummary(QueryFilter(req)) + .chainError("Could not get compliance summary") + .toLiftResponseOne(params, schema, _ => None) + } } } @@ -532,6 +538,20 @@ class ComplianceApi( } } + // TODO: when migrating to scala 3 with implicit context function it should be possible to write by parameterizing with [A: JsonEncoder] + private[this] def withEncodersCtx( + precisionResult: PureResult[CompliancePrecision], + params: DefaultParams, + schema: EndpointSchema + )(body: ComplianceEncoders => LiftResponse): LiftResponse = { + implicit val prettify = params.prettify + precisionResult match { + case Right(precision) => + body(new ComplianceEncoders()(precision)) + case Left(e) => + RudderJsonResponse.internalError(None, RudderJsonResponse.ResponseSchema.fromSchema(schema), e.fullMsg) + } + } } /** @@ -1174,9 +1194,8 @@ class ComplianceAPIService( * Get global and targeted compliance at level 1 (without any details) with global compliance at left and targeted at right */ def getNodeGroupComplianceSummary( - filter: ComplianceAPIService.QueryFilter, - precision: Option[CompliancePrecision] - )(implicit qc: QueryContext): IOResult[List[ByNodeGroupFullCompliance]] = { + filter: ComplianceAPIService.QueryFilter + )(implicit precision: CompliancePrecision, qc: QueryContext): IOResult[List[ByNodeGroupFullCompliance]] = { for { t1 <- currentTimeMillis nodeFacts <- nodeFactRepos.getAll() @@ -1298,7 +1317,16 @@ class ComplianceAPIService( allRuleInfos, level, false - )).map { case (global, targeted) => ByNodeGroupFullCompliance.apply(id, name, cat.name, global, targeted) } + )).map { + case (global, targeted) => + ByNodeGroupFullCompliance.apply( + id, + name, + cat.name, + global.transformInto[GenericCompliance], + targeted.transformInto[GenericCompliance] + ) + } } } yield { filter.apply(fullCompliance) diff --git a/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/rest/RestTestSetUp.scala b/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/rest/RestTestSetUp.scala index ea16432d600..fe0e406b461 100644 --- a/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/rest/RestTestSetUp.scala +++ b/webapp/sources/rudder/rudder-rest/src/test/scala/com/normation/rudder/rest/RestTestSetUp.scala @@ -945,6 +945,7 @@ class RestTestSetUp { campaignApiModule.api, new ComplianceApi( restExtractorService, + zioJsonExtractor, complianceService.complianceAPIService, mockDirectives.directiveRepo ) diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/RudderConfig.scala b/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/RudderConfig.scala index 545429ec917..34537e3ea15 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/RudderConfig.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/RudderConfig.scala @@ -2157,7 +2157,7 @@ object RudderConfigInit { stringUuidGenerator ) val modules = List( - new ComplianceApi(restExtractorService, complianceAPIService, roDirectiveRepository), + new ComplianceApi(restExtractorService, zioJsonExtractor, complianceAPIService, roDirectiveRepository), new GroupsApi( roLdapNodeGroupRepository, restExtractorService,