diff --git a/webapp/sources/pom.xml b/webapp/sources/pom.xml index 71715e4565c..ea1dab12468 100644 --- a/webapp/sources/pom.xml +++ b/webapp/sources/pom.xml @@ -467,7 +467,7 @@ limitations under the License. 1.5.2 0.10.2 24.1.1 - 1.5.0 + 1.6.0 0.7.0 5.5.1 2.3 diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/db/Doobie.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/db/Doobie.scala index 1640f9f1eee..78a8eb270d8 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/db/Doobie.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/db/Doobie.scala @@ -57,6 +57,7 @@ import doobie.util.log.ExecFailure import doobie.util.log.LogEvent import doobie.util.log.ProcessingFailure import doobie.util.transactor +import io.scalaland.chimney.syntax.* import java.sql.SQLXML import javax.sql.DataSource import net.liftweb.common.* @@ -279,15 +280,16 @@ object Doobie { } } + /* + * The 4 following ones are used in udder-core/src/main/scala/com/normation/rudder/repository/jdbc/ComplianceRepository.scala + */ implicit val CompliancePercentWrite: Write[CompliancePercent] = { - import ComplianceLevelSerialisation.* - import net.liftweb.json.* - Write[String].contramap(x => compactRender(x.toJson)) + Write[String].contramap(x => x.transformInto[ComplianceSerializable].toJson) } implicit val ComplianceRunInfoComposite: Write[(RunAnalysis, RunComplianceInfo)] = { import NodeStatusReportSerialization.* - Write[String].contramap(_.toCompactJson) + Write[String].contramap(runToJson) } implicit val AggregatedStatusReportComposite: Write[AggregatedStatusReport] = { @@ -297,7 +299,7 @@ object Doobie { implicit val SetRuleNodeStatusReportComposite: Write[Set[RuleNodeStatusReport]] = { import NodeStatusReportSerialization.* - Write[String].contramap(_.toCompactJson) + Write[String].contramap(ruleNodeStatusReportToJson) } import doobie.enumerated.JdbcType.SqlXml diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/domain/policies/Tags.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/domain/policies/Tags.scala index 2acd8612c68..3596db8d705 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/domain/policies/Tags.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/domain/policies/Tags.scala @@ -36,9 +36,11 @@ */ package com.normation.rudder.domain.policies -import com.normation.rudder.repository.json.DataExtractor.CompleteJson -import com.normation.rudder.repository.json.JsonExtractorUtils -import net.liftweb.common.* +import com.normation.errors.PureResult +import com.normation.errors.Unexpected +import io.scalaland.chimney.* +import io.scalaland.chimney.syntax.* +import zio.json.* /** * Tags that apply on Rules and Directives @@ -64,52 +66,38 @@ final case class Tags(tags: Set[Tag]) extends AnyVal { } object Tags { - // get tags from a list of key/value embodied by a Map with one elements (but - // also works with several elements in map) - def fromMaps(tags: List[Map[String, String]]): Tags = { - Tags(tags.flatMap(_.map { case (k, v) => Tag(TagName(k), TagValue(v)) }).toSet) + private case class JsonTag(key: String, value: String) + implicit private val transformTag: Transformer[Tag, JsonTag] = { case Tag(name, value) => JsonTag(name.value, value.value) } + implicit private val transformJsonTag: Transformer[JsonTag, Tag] = { + case JsonTag(name, value) => Tag(TagName(name), TagValue(value)) } -} -object JsonTagSerialisation { - - import net.liftweb.json.* - import net.liftweb.json.JsonDSL.* - - def serializeTags(tags: Tags): JValue = { - - // sort all the tags by name - val m: JValue = JArray( - tags.tags.toList.sortBy(_.name.value).map(t => ("key" -> t.name.value) ~ ("value" -> t.value.value): JObject) - ) - - m + implicit private val transformTags: Transformer[Tags, List[JsonTag]] = { + case Tags(tags) => tags.toList.sortBy(_.name.value).map(_.transformInto[JsonTag]) + } + implicit private val transformListJsonTag: Transformer[List[JsonTag], Tags] = { + case list => Tags(list.map(_.transformInto[Tag]).toSet) } -} - -trait JsonTagExtractor[M[_]] extends JsonExtractorUtils[M] { - import net.liftweb.json.* + implicit private val codecJsonTag: JsonCodec[JsonTag] = DeriveJsonCodec.gen + implicit val encoderTags: JsonEncoder[Tags] = JsonEncoder.list[JsonTag].contramap(_.transformInto[List[JsonTag]]) + implicit val decoderTags: JsonDecoder[Tags] = JsonDecoder.list[JsonTag].map(_.transformInto[Tags]) - def unserializeTags(value: String): Box[M[Tags]] = { - parseOpt(value) match { - case Some(json) => extractTags(json) - case _ => Failure(s"Invalid JSON serialization for Tags ${value}") - } + // get tags from a list of key/value embodied by a Map with one elements (but + // also works with several elements in map) + def fromMaps(tags: List[Map[String, String]]): Tags = { + Tags(tags.flatMap(_.map { case (k, v) => Tag(TagName(k), TagValue(v)) }).toSet) } - def convertToTag(jsonTag: JValue): Box[Tag] = { - for { - tagName <- CompleteJson.extractJsonString(jsonTag, "key", s => Full(TagName(s))) - tagValue <- CompleteJson.extractJsonString(jsonTag, "value", s => Full(TagValue(s))) - } yield { - Tag(tagName, tagValue) - } - } + val empty: Tags = Tags(Set()) - def extractTags(value: JValue): Box[M[Tags]] = { - extractJsonArray(value, "")(convertToTag).map(k => - monad.map(k)(tags => Tags(tags.toSet)) - ) ?~! s"Invalid JSON serialization for Tags ${value}" + // we have a lot of cases where we parse an `Option[String]` into a `PureResult[Tags]` with + // default value `Tags.empty`. The string format is: + // [{"key":"k1","value":"v1"},{"key":"k2","value":"v2"}] + def parse(opt: Option[String]): PureResult[Tags] = { + opt match { + case Some(v) => v.fromJson[Tags].left.map(Unexpected.apply) + case None => Right(Tags.empty) + } } } diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/domain/reports/ComplianceLevel.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/domain/reports/ComplianceLevel.scala index 4106e4ed389..aeab484f90e 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/domain/reports/ComplianceLevel.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/domain/reports/ComplianceLevel.scala @@ -40,24 +40,22 @@ package com.normation.rudder.domain.reports import com.normation.rudder.domain.reports.ComplianceLevel.PERCENT_PRECISION import com.normation.rudder.domain.reports.CompliancePrecision.Level0 import com.normation.rudder.domain.reports.CompliancePrecision.Level2 +import io.scalaland.chimney.* import net.liftweb.common.* -import net.liftweb.http.js.JE -import net.liftweb.http.js.JE.JsArray -import net.liftweb.json.JsonAST.JInt -import net.liftweb.json.JsonAST.JObject -import net.liftweb.json.JsonAST.JValue import zio.Chunk import zio.json.* +import zio.json.ast.* +import zio.json.ast.Json.* /** - * That file define a "compliance level" object, which store all the kind of reports we can get and + * That file defines a "compliance level" object, which store all the kind of reports we can get and * compute percentage on them. * * Percent are stored a double from 0 to 100 with two relevant digits so 12.34% is actually stored * as Double(12.34) (not 0.1234) * * Since use only two digits in percent, we only have a 10e-4 precision, but we nonetheless NEVER EVER - * want to make 1 error among 20000 success reports disapear by being rounded to 0. + * want to make 1 error among 20000 success reports disappear by being rounded to 0. * So we accept that we have a minimum for all percent, `MIN_PC`, and whatever the real number, it * will be returned as that minimum. * @@ -65,7 +63,7 @@ import zio.json.* * It also mean that any transformation or computation on compliance must always be done on level, never * compliance percent, which are just a model for human convenience, but is false. * - * This class should always be instanciated with CompliancePercent.fromLevels` to ensure sum is 100% + * This class should always be instantiated with CompliancePercent.fromLevels` to ensure sum is 100% * The rounding algorithm is: * - sort levels by number of reports, less first, sum them to get total number of reports * - for each level except the last one (most number): @@ -95,47 +93,24 @@ final case class CompliancePercent( val compliance: Double = success + repaired + notApplicable + compliant + auditNotApplicable } -final case class ComplianceSerializable( - applying: Option[Double], - successNotApplicable: Option[Double], - successAlreadyOK: Option[Double], - successRepaired: Option[Double], - error: Option[Double], - auditCompliant: Option[Double], - auditNonCompliant: Option[Double], - auditError: Option[Double], - auditNotApplicable: Option[Double], - unexpectedUnknownComponent: Option[Double], - unexpectedMissingComponent: Option[Double], - noReport: Option[Double], - reportsDisabled: Option[Double], - badPolicyMode: Option[Double] -) +object CompliancePercent { -object ComplianceSerializable { - def fromPercent(compliancePercent: CompliancePercent): ComplianceSerializable = { - ComplianceSerializable( - if (compliancePercent.pending == 0) None else Some(compliancePercent.pending), - if (compliancePercent.notApplicable == 0) None else Some(compliancePercent.notApplicable), - if (compliancePercent.success == 0) None else Some(compliancePercent.success), - if (compliancePercent.repaired == 0) None else Some(compliancePercent.repaired), - if (compliancePercent.error == 0) None else Some(compliancePercent.error), - if (compliancePercent.compliant == 0) None else Some(compliancePercent.compliant), - if (compliancePercent.nonCompliant == 0) None else Some(compliancePercent.nonCompliant), - if (compliancePercent.auditError == 0) None else Some(compliancePercent.auditError), - if (compliancePercent.auditNotApplicable == 0) None else Some(compliancePercent.auditNotApplicable), - if (compliancePercent.unexpected == 0) None else Some(compliancePercent.unexpected), - if (compliancePercent.missing == 0) None else Some(compliancePercent.missing), - if (compliancePercent.noAnswer == 0) None else Some(compliancePercent.noAnswer), - if (compliancePercent.reportsDisabled == 0) None else Some(compliancePercent.reportsDisabled), - if (compliancePercent.badPolicyMode == 0) None else Some(compliancePercent.badPolicyMode) - ) + implicit val transformComplianceSerializable: Transformer[CompliancePercent, ComplianceSerializable] = { + Transformer + .define[CompliancePercent, ComplianceSerializable] + .withFieldRenamed(_.pending, _.applying) + .withFieldRenamed(_.notApplicable, _.successNotApplicable) + .withFieldRenamed(_.success, _.successAlreadyOK) + .withFieldRenamed(_.repaired, _.successRepaired) + .withFieldRenamed(_.compliant, _.auditCompliant) + .withFieldRenamed(_.nonCompliant, _.auditNonCompliant) + .withFieldRenamed(_.unexpected, _.unexpectedUnknownComponent) + .withFieldRenamed(_.missing, _.unexpectedMissingComponent) + .withFieldRenamed(_.noAnswer, _.noReport) + .buildTransformer } -} -object CompliancePercent { - - // a correspondance array between worse order in `ReportType` and the order of fields in `ComplianceLevel` + // a mapping array between worse order in `ReportType` and the order of fields in `ComplianceLevel` val WORSE_ORDER: Array[Int] = { import ReportType.* Array( @@ -188,7 +163,7 @@ object CompliancePercent { if (total == 0) { // special case: let it be 0 CompliancePercent()(precision) } else { - // these depends on the precision + // these depend on the precision val diviser = divisers(precision.precision) val hundred = hundreds(precision.precision) @@ -236,7 +211,7 @@ object CompliancePercent { if (total == 0) { // special case: let it be 0 0 } else { - // these depends on the precision + // these depend on the precision val diviser = divisers(precision.precision) val hundred = hundreds(precision.precision) @@ -314,7 +289,7 @@ object CompliancePercent { } def sortLevelsWithoutPending(c: ComplianceLevel): List[(Int, Int)] = { - // we want to compare accordingly to `ReportType.getWorsteType` but I don't see any + // we want to compare accordingly to `ReportType.getWorstType` but I don't see any // way to do it directly since we don't use the same order in compliance. // So we map index of a compliance element to it's worse type order and compare by index @@ -391,7 +366,8 @@ final case class ComplianceLevel( pending + success + repaired + error + unexpected + missing + noAnswer + notApplicable + reportsDisabled + compliant + auditNotApplicable + nonCompliant + auditError + badPolicyMode lazy val total_ok: Int = success + repaired + notApplicable + compliant + auditNotApplicable - def withoutPending: ComplianceLevel = this.copy(pending = 0, reportsDisabled = 0) + def withoutPending: ComplianceLevel = this.copy(pending = 0, reportsDisabled = 0) + def computePercent(precision: CompliancePrecision = PERCENT_PRECISION): CompliancePercent = CompliancePercent.fromLevels(this, precision) @@ -446,6 +422,21 @@ object CompliancePrecision { } object ComplianceLevel { + implicit val transformComplianceSerializable: Transformer[ComplianceLevel, ComplianceLevelSerialisation] = { + Transformer + .define[ComplianceLevel, ComplianceLevelSerialisation] + .withFieldRenamed(_.pending, _.applying) + .withFieldRenamed(_.notApplicable, _.successNotApplicable) + .withFieldRenamed(_.success, _.successAlreadyOK) + .withFieldRenamed(_.repaired, _.successRepaired) + .withFieldRenamed(_.compliant, _.auditCompliant) + .withFieldRenamed(_.nonCompliant, _.auditNonCompliant) + .withFieldRenamed(_.unexpected, _.unexpectedUnknownComponent) + .withFieldRenamed(_.missing, _.unexpectedMissingComponent) + .withFieldRenamed(_.noAnswer, _.noReport) + .buildTransformer + } + def PERCENT_PRECISION = Level2 def compute(reports: Iterable[ReportType]): ComplianceLevel = { @@ -468,23 +459,21 @@ object ComplianceLevel { var auditError = 0 var badPolicyMode = 0 - reports.foreach { report => - report match { - case EnforceNotApplicable => notApplicable += 1 - case EnforceSuccess => success += 1 - case EnforceRepaired => repaired += 1 - case EnforceError => error += 1 - case Unexpected => unexpected += 1 - case Missing => missing += 1 - case NoAnswer => noAnswer += 1 - case Pending => pending += 1 - case Disabled => reportsDisabled += 1 - case AuditCompliant => compliant += 1 - case AuditNotApplicable => auditNotApplicable += 1 - case AuditNonCompliant => nonCompliant += 1 - case AuditError => auditError += 1 - case BadPolicyMode => badPolicyMode += 1 - } + reports.foreach { + case EnforceNotApplicable => notApplicable += 1 + case EnforceSuccess => success += 1 + case EnforceRepaired => repaired += 1 + case EnforceError => error += 1 + case Unexpected => unexpected += 1 + case Missing => missing += 1 + case NoAnswer => noAnswer += 1 + case Pending => pending += 1 + case Disabled => reportsDisabled += 1 + case AuditCompliant => compliant += 1 + case AuditNotApplicable => auditNotApplicable += 1 + case AuditNonCompliant => nonCompliant += 1 + case AuditError => auditError += 1 + case BadPolicyMode => badPolicyMode += 1 } ComplianceLevel( pending = pending, @@ -560,79 +549,127 @@ object ComplianceLevel { } } -object ComplianceLevelSerialisation { - import net.liftweb.json.JsonDSL.* - - // utility class to alway have the same names in JSON, - // even if we are refactoring ComplianceLevel at some point - // also remove 0 - private def toJObject( - pending: Number, - success: Number, - repaired: Number, - error: Number, - unexpected: Number, - missing: Number, - noAnswer: Number, - notApplicable: Number, - reportsDisabled: Number, - compliant: Number, - auditNotApplicable: Number, - nonCompliant: Number, - auditError: Number, - badPolicyMode: Number - ) = { - def POS(n: Number) = if (n.doubleValue <= 0) None else Some(JE.Num(n)) - - ( - ("pending" -> POS(pending)) - ~ ("success" -> POS(success)) - ~ ("repaired" -> POS(repaired)) - ~ ("error" -> POS(error)) - ~ ("unexpected" -> POS(unexpected)) - ~ ("missing" -> POS(missing)) - ~ ("noAnswer" -> POS(noAnswer)) - ~ ("notApplicable" -> POS(notApplicable)) - ~ ("reportsDisabled" -> POS(reportsDisabled)) - ~ ("compliant" -> POS(compliant)) - ~ ("auditNotApplicable" -> POS(auditNotApplicable)) - ~ ("nonCompliant" -> POS(nonCompliant)) - ~ ("auditError" -> POS(auditError)) - ~ ("badPolicyMode" -> POS(badPolicyMode)) - ) +// for serialization + +// utility class to always have the same names in JSON, +// even if we are refactoring ComplianceLevel at some point + +// Remove 0 (and neg values) by changing them into None +// Ensure that only some are written. All intermediary objects are removed (ie it's the same as mapping int/double) +final class OptPosNum(val value: Option[Num]) +object OptPosNum { + val none = new OptPosNum(None) + + def apply(i: Int): OptPosNum = if (i <= 0) none else new OptPosNum(Some(Num(i))) + def apply(i: Double): OptPosNum = if (i <= 0) none else new OptPosNum(Some(Num(i))) + def apply(i: java.math.BigDecimal): OptPosNum = if (i.signum() <= 0) none else new OptPosNum(Some(Num(i))) + + implicit def encoderOptPosNum: JsonEncoder[OptPosNum] = JsonEncoder.option[Num].contramap(_.value) + implicit def decoderOptPosNum: JsonDecoder[OptPosNum] = + JsonDecoder.option[Num].map(x => OptPosNum.apply(x.map(_.value).getOrElse(java.math.BigDecimal.ZERO))) + + implicit val transformDouble: Iso[Double, OptPosNum] = Iso[Double, OptPosNum]( + (src: Double) => OptPosNum(src), + (src: OptPosNum) => src.value.map(_.value.doubleValue()).getOrElse(0) + ) + + implicit val transformInt: Iso[Int, OptPosNum] = Iso[Int, OptPosNum]( + (src: Int) => OptPosNum(src), + (src: OptPosNum) => src.value.map(_.value.intValue()).getOrElse(0) + ) + +} + +/* + * This one is the one that is serialized to JSON when we don't use the Array[Array[Int]]. + * The field names must be the one expected by API/client side. Order matters. Name matters. + */ +final case class ComplianceSerializable( + applying: OptPosNum, + successNotApplicable: OptPosNum, + successAlreadyOK: OptPosNum, + successRepaired: OptPosNum, + error: OptPosNum, + auditCompliant: OptPosNum, + auditNonCompliant: OptPosNum, + auditError: OptPosNum, + auditNotApplicable: OptPosNum, + unexpectedUnknownComponent: OptPosNum, + unexpectedMissingComponent: OptPosNum, + noReport: OptPosNum, + reportsDisabled: OptPosNum, + badPolicyMode: OptPosNum +) + +object ComplianceSerializable { + + // A ComplianceSerializable with all field set to OptPosNum.none + def empty = { + import shapeless.syntax.sized.* + val nbFields = 14 + val x = Array.fill(nbFields)(OptPosNum.none).toList + ComplianceSerializable.apply.tupled(x.sized(nbFields).map(_.tupled).get) } - private def parse[T](json: JValue, convert: BigInt => T) = { - def N(n: JValue): T = convert(n match { - case JInt(i) => i - case _ => 0 - }) - - ( - N(json \ "pending"), - N(json \ "success"), - N(json \ "repaired"), - N(json \ "error"), - N(json \ "unexpected"), - N(json \ "missing"), - N(json \ "noAnswer"), - N(json \ "notApplicable"), - N(json \ "reportsDisabled"), - N(json \ "compliant"), - N(json \ "auditNotApplicable"), - N(json \ "nonCompliant"), - N(json \ "auditError"), - N(json \ "badPolicyMode") - ) + implicit val codecComplianceSerializable: JsonCodec[ComplianceSerializable] = DeriveJsonCodec.gen + implicit val transformComplianceSerializable: Transformer[ComplianceSerializable, CompliancePercent] = { + Transformer + .define[ComplianceSerializable, CompliancePercent] + .withFieldConst(_.precision, Level0) + .withFieldRenamed(_.applying, _.pending) + .withFieldRenamed(_.successNotApplicable, _.notApplicable) + .withFieldRenamed(_.successAlreadyOK, _.success) + .withFieldRenamed(_.successRepaired, _.repaired) + .withFieldRenamed(_.auditCompliant, _.compliant) + .withFieldRenamed(_.auditNonCompliant, _.nonCompliant) + .withFieldRenamed(_.unexpectedUnknownComponent, _.unexpected) + .withFieldRenamed(_.unexpectedMissingComponent, _.missing) + .withFieldRenamed(_.noReport, _.noAnswer) + .buildTransformer } +} - def parseLevel(json: JValue): ComplianceLevel = { - (ComplianceLevel.apply _).tupled(parse(json, (i: BigInt) => i.intValue)) +final case class ComplianceLevelSerialisation( + applying: OptPosNum, + successNotApplicable: OptPosNum, + successAlreadyOK: OptPosNum, + successRepaired: OptPosNum, + error: OptPosNum, + auditCompliant: OptPosNum, + auditNonCompliant: OptPosNum, + auditError: OptPosNum, + auditNotApplicable: OptPosNum, + unexpectedUnknownComponent: OptPosNum, + unexpectedMissingComponent: OptPosNum, + noReport: OptPosNum, + reportsDisabled: OptPosNum, + badPolicyMode: OptPosNum +) + +object ComplianceLevelSerialisation { + + implicit val codecComplianceLevelSerialisation: JsonCodec[ComplianceLevelSerialisation] = DeriveJsonCodec.gen + + implicit val transformComplianceLevelSerialisation: Transformer[ComplianceLevelSerialisation, ComplianceLevel] = { + Transformer + .define[ComplianceLevelSerialisation, ComplianceLevel] + .withFieldRenamed(_.applying, _.pending) + .withFieldRenamed(_.successNotApplicable, _.notApplicable) + .withFieldRenamed(_.successAlreadyOK, _.success) + .withFieldRenamed(_.successRepaired, _.repaired) + .withFieldRenamed(_.auditCompliant, _.compliant) + .withFieldRenamed(_.auditNonCompliant, _.nonCompliant) + .withFieldRenamed(_.unexpectedUnknownComponent, _.unexpected) + .withFieldRenamed(_.unexpectedMissingComponent, _.missing) + .withFieldRenamed(_.noReport, _.noAnswer) + .buildTransformer } - // transform the compliance percent to a list with a given order: - // pc_reportDisabled, pc_notapplicable, pc_success, pc_repaired, - // pc_error, pc_pending, pc_noAnswer, pc_missing, pc_unknown + // transform the compliance percent to an array with a given order: + // reportDisabled, notapplicable, success, repaired, + // error, pending, noAnswer, missing, unexpected, + // auditNotApplicable, compliant, nonCompliant, auditError, + // badPolicyMode object array { implicit val complianceLevelArrayEncoder: JsonEncoder[ComplianceLevel] = { JsonEncoder[Chunk[(Int, Double)]].contramap(compliance => { @@ -673,83 +710,36 @@ object ComplianceLevelSerialisation { // same as in "array" but in old lift-json AST, should be removed soon implicit class ComplianceLevelToJs(val compliance: ComplianceLevel) extends AnyVal { - def toJsArray: JsArray = { + def toJsArray: Json.Arr = { val pc = compliance.computePercent() - JsArray( - JsArray(compliance.reportsDisabled, JE.Num(pc.reportsDisabled)), // 0 - - JsArray(compliance.notApplicable, JE.Num(pc.notApplicable)), // 1 + Arr( + Arr(Num(compliance.reportsDisabled), Num(pc.reportsDisabled)), // 0 - JsArray(compliance.success, JE.Num(pc.success)), // 2 + Arr(Num(compliance.notApplicable), Num(pc.notApplicable)), // 1 - JsArray(compliance.repaired, JE.Num(pc.repaired)), // 3 + Arr(Num(compliance.success), Num(pc.success)), // 2 - JsArray(compliance.error, JE.Num(pc.error)), // 4 + Arr(Num(compliance.repaired), Num(pc.repaired)), // 3 - JsArray(compliance.pending, JE.Num(pc.pending)), // 5 + Arr(Num(compliance.error), Num(pc.error)), // 4 - JsArray(compliance.noAnswer, JE.Num(pc.noAnswer)), // 6 + Arr(Num(compliance.pending), Num(pc.pending)), // 5 - JsArray(compliance.missing, JE.Num(pc.missing)), // 7 + Arr(Num(compliance.noAnswer), Num(pc.noAnswer)), // 6 - JsArray(compliance.unexpected, JE.Num(pc.unexpected)), // 8 + Arr(Num(compliance.missing), Num(pc.missing)), // 7 - JsArray(compliance.auditNotApplicable, JE.Num(pc.auditNotApplicable)), // 9 + Arr(Num(compliance.unexpected), Num(pc.unexpected)), // 8 - JsArray(compliance.compliant, JE.Num(pc.compliant)), // 10 + Arr(Num(compliance.auditNotApplicable), Num(pc.auditNotApplicable)), // 9 - JsArray(compliance.nonCompliant, JE.Num(pc.nonCompliant)), // 11 + Arr(Num(compliance.compliant), Num(pc.compliant)), // 10 - JsArray(compliance.auditError, JE.Num(pc.auditError)), // 12 + Arr(Num(compliance.nonCompliant), Num(pc.nonCompliant)), // 11 - JsArray(compliance.badPolicyMode, JE.Num(pc.badPolicyMode)) // 13 - ) - } - - def toJson: JObject = { - import compliance.* - toJObject( - pending, - success, - repaired, - error, - unexpected, - missing, - noAnswer, - notApplicable, - reportsDisabled, - compliant, - auditNotApplicable, - nonCompliant, - auditError, - badPolicyMode - ) - } - } + Arr(Num(compliance.auditError), Num(pc.auditError)), // 12 - // transform a compliace percent to JSON. - // here, we are using attributes contrary to compliance level, - // and we only keep the one > 0 (we want the result to be - // human-readable and to aknolewdge the fact that there may be - // new fields. - implicit class CompliancePercentToJs(val c: CompliancePercent) extends AnyVal { - def toJson: JObject = { - import c.* - toJObject( - pending, - success, - repaired, - error, - unexpected, - missing, - noAnswer, - notApplicable, - reportsDisabled, - compliant, - auditNotApplicable, - nonCompliant, - auditError, - badPolicyMode + Arr(Num(compliance.badPolicyMode), Num(pc.badPolicyMode)) // 13 ) } } diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/domain/reports/StatusReports.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/domain/reports/StatusReports.scala index b9a812258f7..ab4717f9e20 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/domain/reports/StatusReports.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/domain/reports/StatusReports.scala @@ -44,10 +44,15 @@ import com.normation.rudder.domain.policies.* import com.softwaremill.quicklens.* import enumeratum.Enum import enumeratum.EnumEntry +import io.scalaland.chimney.* +import io.scalaland.chimney.syntax.* import net.liftweb.common.Loggable import org.joda.time.DateTime +import scala.annotation.nowarn import scala.collection.MapView -import zio.Chunk +import zio.* +import zio.json.* +import zio.json.ast.Json.* /** * That file contains all the kind of status reports for: @@ -574,105 +579,208 @@ object MessageStatusReport { } +/* + * This is the serialization for API. + * We are again redefining roughly the same objects. + */ +// not sure why it doesn't see that they are used +@nowarn("msg=private val .* in object NodeStatusReportSerialization is never used") object NodeStatusReportSerialization { - import net.liftweb.json.* - import net.liftweb.json.JsonDSL.* - - def jsonRunInfo(runInfo: RunAnalysis): JValue = { - (("type" -> runInfo.kind.entryName) - ~ ("expectedConfigId" -> runInfo.expectedConfigId.map(_.value)) - ~ ("runConfigId" -> runInfo.lastRunConfigId.map(_.value))) + private case class ApiRunInfo( + `type`: String, + expectedConfigId: Option[String], + runConfigId: Option[String] + ) + implicit private lazy val encoderApiRunInfo: JsonEncoder[ApiRunInfo] = DeriveJsonEncoder.gen + implicit private lazy val transformRunAnalysis: Transformer[RunAnalysis, ApiRunInfo] = { + Transformer + .define[RunAnalysis, ApiRunInfo] + .withFieldComputed(_.`type`, _.kind.entryName) + .withFieldComputed(_.expectedConfigId, _.expectedConfigId.map(_.value)) + .withFieldComputed(_.runConfigId, _.lastRunConfigId.map(_.value)) + .buildTransformer } - def jsonStatusInfo(statusInfo: RunComplianceInfo): JValue = { - ( - ("status" -> (statusInfo match { - case RunComplianceInfo.OK => "success" - case _ => "error" - })) ~ ( - "errors" -> (statusInfo match { + private case class ApiStatusInfo( + status: String, + errors: Option[List[Obj]] + ) + implicit private lazy val encoderApiStatusInfo: JsonEncoder[ApiStatusInfo] = DeriveJsonEncoder.gen + implicit private lazy val transformRunComplianceInfo: Transformer[RunComplianceInfo, ApiStatusInfo] = { + Transformer + .define[RunComplianceInfo, ApiStatusInfo] + .withFieldComputed( + _.status, + { + case RunComplianceInfo.OK => "success" + case _ => "error" + } + ) + .withFieldComputed( + _.errors, + { case RunComplianceInfo.OK => None case RunComplianceInfo.PolicyModeInconsistency(errors) => Some(errors.map { case RunComplianceInfo.PolicyModeError.TechniqueMixedMode(msg) => - ("policyModeError" -> ("message" -> msg)): JObject + Obj(("policyModeError", Obj("message" -> Str(msg)))) case RunComplianceInfo.PolicyModeError.AgentAbortMessage(cause, msg) => - ("policyModeInconsistency" -> (("cause" -> cause) ~ ("message" -> msg))): JObject + Obj(("policyModeInconsistency", Obj(("cause", Str(cause)), ("message", Str(msg))))) }) - }) + } ) - ) + .buildTransformer } - implicit class RunComplianceInfoToJs(val x: (RunAnalysis, RunComplianceInfo)) extends AnyVal { - def toJValue: JObject = { - ( - ("run" -> jsonRunInfo(x._1)) - ~ ("status" -> jsonStatusInfo(x._2)) - ) - } + private case class ApiInfo( + run: ApiRunInfo, + status: ApiStatusInfo + ) + + implicit private lazy val encoderApiInfo: JsonEncoder[ApiInfo] = DeriveJsonEncoder.gen + implicit private lazy val transformApiInfo: Transformer[(RunAnalysis, RunComplianceInfo), ApiInfo] = { + Transformer + .define[(RunAnalysis, RunComplianceInfo), ApiInfo] + .withFieldComputed(_.run, _._1.transformInto[ApiRunInfo]) + .withFieldComputed(_.status, _._2.transformInto[ApiStatusInfo]) + .buildTransformer + } + + // entry point for Doobie + def runToJson(p: (RunAnalysis, RunComplianceInfo)): String = p.transformInto[ApiInfo].toJson + + private case class ApiComponentValue( + componentName: String, + compliance: ComplianceSerializable, + numberReports: Int, + value: List[ApiValue] + ) + + // here, I'm not sure that we want compliance or + // compliance percents. Having a normalized value + // seems far better for queries in the future. + // but in that case, we should also keep the total + // number of events to be able to rebuild raw data + + // always map compliance field from ComplianceLevel to ComplianceSerializable automatically + implicit private lazy val transformComplianceLevel: Transformer[ComplianceLevel, ComplianceSerializable] = { + case c: ComplianceLevel => c.computePercent().transformInto[ComplianceSerializable] + } + + implicit private lazy val encoderApiComponentValue: JsonEncoder[ApiComponentValue] = DeriveJsonEncoder.gen + implicit private lazy val transformValueStatusReport: Transformer[ValueStatusReport, ApiComponentValue] = { + Transformer + .define[ValueStatusReport, ApiComponentValue] + .withFieldComputed(_.numberReports, _.compliance.total) + .withFieldComputed(_.value, _.componentValues.map(_.transformInto[ApiValue])) + .buildTransformer + } + + private case class ApiValue( + value: String, + compliance: ComplianceSerializable, + numberReports: Int, + unexpanded: String, + messages: List[ApiMessage] + ) - def toJson: String = prettyRender(toJValue) - def toCompactJson: String = compactRender(toJValue) + implicit private lazy val encoderApiValue: JsonEncoder[ApiValue] = DeriveJsonEncoder.gen + + implicit private lazy val transformValue: Transformer[ComponentValueStatusReport, ApiValue] = { + Transformer + .define[ComponentValueStatusReport, ApiValue] + .withFieldComputed(_.value, _.componentValue) + .withFieldComputed(_.numberReports, _.compliance.total) + .withFieldComputed(_.unexpanded, _.expectedComponentValue) + .buildTransformer + } + + private case class ApiMessage( + message: Option[String], + `type`: String + ) + + implicit private lazy val encoderApiMessage: JsonEncoder[ApiMessage] = DeriveJsonEncoder.gen + implicit private lazy val transformMessageStatusReport: Transformer[MessageStatusReport, ApiMessage] = { + Transformer + .define[MessageStatusReport, ApiMessage] + .withFieldComputed(_.`type`, _.reportType.severity) + .buildTransformer + } + + private case class ApiComponentBlock( + value: String, + compliance: ComplianceSerializable, + numberReports: Int, + subComponents: List[Either[ApiComponentValue, ApiComponentBlock]], + reportingLogic: String + ) + + implicit private lazy val encoderApiComponentBlock: JsonEncoder[ApiComponentBlock] = DeriveJsonEncoder.gen + implicit private lazy val transformComponent + : Transformer[ComponentStatusReport, Either[ApiComponentValue, ApiComponentBlock]] = { + case b: BlockStatusReport => Right(b.transformInto[ApiComponentBlock]) + case v: ValueStatusReport => Left(v.transformInto[ApiComponentValue]) + } + + implicit private lazy val transformBlockStatusReport: Transformer[BlockStatusReport, ApiComponentBlock] = { + Transformer + .define[BlockStatusReport, ApiComponentBlock] + .withFieldComputed(_.value, _.componentName) + .withFieldComputed(_.numberReports, _.compliance.total) + .withFieldComputed(_.reportingLogic, _.reportingLogic.value) + .enableMethodAccessors + .buildTransformer } + private case class ApiDirective( + directiveId: String, + compliance: ComplianceSerializable, + numberReports: Int, + components: List[Either[ApiComponentValue, ApiComponentBlock]] + ) + + implicit private lazy val encoderApiDirective: JsonEncoder[ApiDirective] = DeriveJsonEncoder.gen + implicit private lazy val transformDirectiveStatusReport: Transformer[DirectiveStatusReport, ApiDirective] = { + Transformer + .define[DirectiveStatusReport, ApiDirective] + .withFieldComputed(_.directiveId, _.directiveId.serialize) + .withFieldComputed(_.numberReports, _.compliance.total) + .buildTransformer + } + + private case class ApiRule( + ruleId: String, + compliance: ComplianceSerializable, + numberReports: Int, + directives: List[ApiDirective] + ) + + implicit private lazy val encoderApiRule: JsonEncoder[ApiRule] = DeriveJsonEncoder.gen + implicit private lazy val transformRule: Transformer[RuleNodeStatusReport, ApiRule] = { + Transformer + .define[RuleNodeStatusReport, ApiRule] + .withFieldComputed(_.ruleId, _.ruleId.serialize) + .withFieldComputed(_.numberReports, _.compliance.total) + .withFieldComputed(_.directives, _.directives.toList.sortBy(_._1.serialize).map(_._2.transformInto[ApiDirective])) + .buildTransformer + } + + private case class ApiAggregated(rules: List[ApiRule]) + + implicit private lazy val encoderApiAggregated: JsonEncoder[ApiAggregated] = DeriveJsonEncoder.gen + implicit private lazy val transformAggregatedStatusReport: Transformer[AggregatedStatusReport, ApiAggregated] = { + case a: AggregatedStatusReport => ApiAggregated(a.reports.toList.map(_.transformInto[ApiRule])) + } + + // entry point for Doobie + def ruleNodeStatusReportToJson(r: Set[RuleNodeStatusReport]) = r.toList.map(_.transformInto[ApiRule]).toJson + + // main external entry point implicit class AggregatedStatusReportToJs(val x: AggregatedStatusReport) extends AnyVal { - def toJValue: JValue = x.reports.toJValue - def toJson: String = prettyRender(toJValue) - def toCompactJson: String = compactRender(toJValue) - } - - implicit class SetRuleNodeStatusReportToJs(reports: Set[RuleNodeStatusReport]) { - import ComplianceLevelSerialisation.* - - def componentValueToJson(c: ComponentStatusReport): JValue = { - c match { - case c: ValueStatusReport => - (("componentName" -> c.componentName) - ~ ("compliance" -> c.compliance.computePercent().toJson) - ~ ("numberReports" -> c.compliance.total) - ~ ("values" -> c.componentValues.map { v => - (("value" -> v.componentValue) - ~ ("compliance" -> v.compliance.computePercent().toJson) - ~ ("numberReports" -> v.compliance.total) - ~ ("unexpanded" -> v.expectedComponentValue) - ~ ("messages" -> v.messages.map { m => - (("message" -> m.message) - ~ ("type" -> m.reportType.severity)) - })) - })) - case c: BlockStatusReport => - (("componentName" -> c.componentName) - ~ ("compliance" -> c.compliance.computePercent().toJson) - ~ ("numberReports" -> c.compliance.total) - ~ ("subComponents" -> c.subComponents.map(componentValueToJson)) - ~ ("reportingLogic" -> c.reportingLogic.toString)) - } - } - def toJValue: JValue = { - - // here, I'm not sure that we want compliance or - // compliance percents. Having a normalized value - // seems far better for queries in the futur. - // but in that case, we should also keep the total - // number of events to be able to rebuild raw data - - "rules" -> reports.map { r => - (("ruleId" -> r.ruleId.serialize) - ~ ("compliance" -> r.compliance.computePercent().toJson) - ~ ("numberReports" -> r.compliance.total) - ~ ("directives" -> r.directives.values.map { d => - (("directiveId" -> d.directiveId.serialize) - ~ ("compliance" -> d.compliance.computePercent().toJson) - ~ ("numberReports" -> d.compliance.total) - ~ ("components" -> d.components.map(componentValueToJson))) - })) - } - } - - def toJson: String = prettyRender(toJValue) - def toCompactJson: String = compactRender(toJValue) + def toPrettyJson: String = x.transformInto[ApiAggregated].toJsonPretty + def toCompactJson: String = x.transformInto[ApiAggregated].toJson } } diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/json/JsonExctractorUtils.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/json/JsonExctractorUtils.scala index e251f24bc38..bac4e9197cb 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/json/JsonExctractorUtils.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/json/JsonExctractorUtils.scala @@ -39,7 +39,6 @@ package com.normation.rudder.repository.json import cats.* import cats.implicits.* -import com.normation.rudder.domain.policies.JsonTagExtractor import com.normation.utils.Control.* import net.liftweb.common.* import net.liftweb.json.* @@ -133,7 +132,7 @@ trait JsonExtractorUtils[A[_]] { } } -trait DataExtractor[T[_]] extends JsonTagExtractor[T] +trait DataExtractor[T[_]] extends JsonExtractorUtils[T] object DataExtractor { object OptionnalJson extends DataExtractor[Option] { diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPDiffMapper.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPDiffMapper.scala index 6acbb444bc0..22312b76121 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPDiffMapper.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPDiffMapper.scala @@ -58,7 +58,6 @@ import com.normation.rudder.domain.properties.GroupProperty import com.normation.rudder.domain.properties.InheritMode import com.normation.rudder.domain.properties.ModifyGlobalParameterDiff import com.normation.rudder.domain.properties.PropertyProvider -import com.normation.rudder.repository.json.DataExtractor import com.normation.rudder.rule.category.RuleCategoryId import com.normation.rudder.services.queries.* import com.unboundid.ldap.sdk.DN @@ -174,12 +173,9 @@ class LDAPDiffMapper( case A_SERIALIZED_TAGS => for { d <- diff - tags <- mod.getOptValue() match { - case Some(v) => DataExtractor.CompleteJson.unserializeTags(v).map(_.tags).toPureResult - case None => Right(Set[Tag]()) - } + tags <- Tags.parse(mod.getOptValue()) } yield { - d.copy(modTags = Some(SimpleDiff(oldCr.tags.tags, tags))) + d.copy(modTags = Some(SimpleDiff(oldCr.tags.tags, tags.tags))) } case x => Left(Err.UnexpectedObject("Unknown diff attribute: " + x)) } @@ -336,10 +332,7 @@ class LDAPDiffMapper( case A_SERIALIZED_TAGS => for { d <- diff - tags <- mod.getOptValue() match { - case Some(v) => DataExtractor.CompleteJson.unserializeTags(v).toPureResult - case None => Right(Tags(Set())) - } + tags <- Tags.parse(mod.getOptValue()) } yield { d.copy(modTags = Some(SimpleDiff(oldPi.tags, tags))) } diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPEntityMapper.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPEntityMapper.scala index dcdb396bf82..05286ecc64e 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPEntityMapper.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPEntityMapper.scala @@ -75,7 +75,6 @@ import com.normation.rudder.domain.properties.PropertyProvider import com.normation.rudder.facts.nodes.NodeSecurityContext import com.normation.rudder.facts.nodes.SecurityTag import com.normation.rudder.reports.* -import com.normation.rudder.repository.json.DataExtractor.CompleteJson import com.normation.rudder.rule.category.RuleCategory import com.normation.rudder.rule.category.RuleCategoryId import com.normation.rudder.services.queries.* @@ -766,14 +765,7 @@ class LDAPEntityMapper( priority = e.getAsInt(A_PRIORITY).getOrElse(0) isEnabled = e.getAsBoolean(A_IS_ENABLED).getOrElse(false) isSystem = e.getAsBoolean(A_IS_SYSTEM).getOrElse(false) - tags <- e(A_SERIALIZED_TAGS) match { - case None => Right(Tags(Set())) - case Some(tags) => - CompleteJson - .unserializeTags(tags) - .toPureResult - .chainError(s"Invalid attribute value for tags ${A_SERIALIZED_TAGS}: ${tags}") - } + tags <- Tags.parse(e(A_SERIALIZED_TAGS)).chainError(s"Invalid attribute value for tags ${A_SERIALIZED_TAGS}") } yield { Directive( DirectiveId(DirectiveUid(id), ParseRev(e(A_REV_ID))), @@ -812,7 +804,7 @@ class LDAPEntityMapper( entry.resetValuesTo(A_IS_ENABLED, directive.isEnabled.toLDAPString) entry.resetValuesTo(A_IS_SYSTEM, directive.isSystem.toLDAPString) directive.policyMode.foreach(mode => entry.resetValuesTo(A_POLICY_MODE, mode.name)) - entry.resetValuesTo(A_SERIALIZED_TAGS, net.liftweb.json.compactRender(JsonTagSerialisation.serializeTags(directive.tags))) + entry.resetValuesTo(A_SERIALIZED_TAGS, directive.tags.toJson) entry } @@ -858,14 +850,7 @@ class LDAPEntityMapper( if (e.isA(OC_RULE)) { for { id <- e.required(A_RULE_UUID) - tags <- e(A_SERIALIZED_TAGS) match { - case None => Right(Tags(Set())) - case Some(tags) => - CompleteJson - .unserializeTags(tags) - .toPureResult - .chainError(s"Invalid attribute value for tags ${A_SERIALIZED_TAGS}: ${tags}") - } + tags <- Tags.parse(e(A_SERIALIZED_TAGS)).chainError(s"Invalid attribute value for tags ${A_SERIALIZED_TAGS}") } yield { val targets = for { target <- e.valuesFor(A_RULE_TARGET) @@ -925,7 +910,7 @@ class LDAPEntityMapper( entry.resetValuesTo(A_DIRECTIVE_UUID, rule.directiveIds.map(_.serialize).toSeq*) entry.resetValuesTo(A_DESCRIPTION, rule.shortDescription) entry.resetValuesTo(A_LONG_DESCRIPTION, rule.longDescription.toString) - entry.resetValuesTo(A_SERIALIZED_TAGS, net.liftweb.json.compactRender(JsonTagSerialisation.serializeTags(rule.tags))) + entry.resetValuesTo(A_SERIALIZED_TAGS, rule.tags.toJson) entry } diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/ComplianceScore.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/ComplianceScore.scala index b21ec79800b..e9083cd146b 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/ComplianceScore.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/score/ComplianceScore.scala @@ -43,6 +43,7 @@ import com.normation.inventory.domain.NodeId import com.normation.rudder.domain.reports.ComplianceSerializable import com.normation.rudder.score.ComplianceScore.scoreId import com.normation.rudder.score.ScoreValue.* +import io.scalaland.chimney.syntax.TransformerOps import zio.* import zio.json.* import zio.syntax.* @@ -52,20 +53,19 @@ object ComplianceScore { } object ComplianceScoreEventHandler extends ScoreEventHandler { - implicit val compliancePercentEncoder: JsonEncoder[ComplianceSerializable] = DeriveJsonEncoder.gen - def handle(event: ScoreEvent): PureResult[List[(NodeId, List[Score])]] = { + def handle(event: ScoreEvent): PureResult[List[(NodeId, List[Score])]] = { event match { case ComplianceScoreEvent(n, percent) => - val p = ComplianceSerializable.fromPercent(percent) + val p = percent.transformInto[ComplianceSerializable] (for { json <- p.toJsonAST } yield { val score = { - if (p == ComplianceSerializable(None, None, None, None, None, None, None, None, None, None, None, None, None, None)) { + if (p == ComplianceSerializable.empty) { Score(scoreId, NoScore, "No rules applied on this node", json) } else { - import ComplianceScore.scoreId + import com.normation.rudder.score.ComplianceScore.scoreId if (percent.compliance >= 95) { Score(scoreId, A, "Compliance is over 95%", json) } else if (percent.compliance >= 80) { diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/services/quicksearch/QuickSearchBackendImpl.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/services/quicksearch/QuickSearchBackendImpl.scala index d29d6511aca..628e9ef47a7 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/services/quicksearch/QuickSearchBackendImpl.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/services/quicksearch/QuickSearchBackendImpl.scala @@ -49,6 +49,7 @@ import com.normation.rudder.domain.RudderDit import com.normation.rudder.domain.RudderLDAPConstants.* import com.normation.rudder.domain.policies.Tag import com.normation.rudder.domain.policies.TagName +import com.normation.rudder.domain.policies.Tags as TAGS import com.normation.rudder.domain.policies.TagValue import com.normation.rudder.domain.properties.NodeProperty import com.normation.rudder.facts.nodes.CoreNodeFact @@ -61,13 +62,12 @@ import com.normation.rudder.ncf.MethodBlock import com.normation.rudder.ncf.MethodCall import com.normation.rudder.ncf.MethodElem import com.normation.rudder.repository.RoDirectiveRepository -import com.normation.rudder.repository.json.DataExtractor.CompleteJson import com.unboundid.ldap.sdk.Attribute import com.unboundid.ldap.sdk.Filter import java.util.regex.Pattern -import net.liftweb.common.Full import net.liftweb.common.Loggable import scala.util.control.NonFatal +import zio.json.* import zio.syntax.* /** @@ -617,12 +617,10 @@ object QSLdapBackend { def transform(pattern: Pattern, value: String): Option[String] = { def parseTag(value: String, matcher: Tag => Boolean, transform: Tag => String) = { - import net.liftweb.json.parse try { - val json = parse(value) - CompleteJson.extractTags(json) match { - case Full(tags) => tags.tags.collectFirst { case t if matcher(t) => transform(t) } - case _ => None + value.fromJson[TAGS] match { + case Right(tags) => tags.tags.collectFirst { case t if matcher(t) => transform(t) } + case _ => None } } catch { case NonFatal(ex) => None diff --git a/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/domain/policies/TagsTest.scala b/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/domain/policies/TagsTest.scala index 995b5529195..93e21430474 100644 --- a/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/domain/policies/TagsTest.scala +++ b/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/domain/policies/TagsTest.scala @@ -36,15 +36,11 @@ */ package com.normation.rudder.domain.policies -import com.normation.rudder.repository.json.DataExtractor.CompleteJson import net.liftweb.common.* -import net.liftweb.json.JsonAST.JArray -import net.liftweb.json.JsonAST.JField -import net.liftweb.json.JsonAST.JObject -import net.liftweb.json.JsonAST.JString import org.junit.runner.RunWith import org.specs2.mutable.* import org.specs2.runner.* +import zio.json.* @RunWith(classOf[JUnitRunner]) class TagsTest extends Specification with Loggable { @@ -60,14 +56,7 @@ class TagsTest extends Specification with Loggable { val simpleTags: Tags = Tags(Set[Tag](tag1, tag2, tag3)) val simpleSerialization = """[{"key":"tag1","value":"tag1-value"},{"key":"tag2","value":"tag2-value"},{"key":"tag3","value":"tag3-value"}]""" - val jsonSimple: JArray = { - JArray( - JObject(JField("key", JString("tag1")), JField("value", JString("tag1-value"))) :: - JObject(JField("key", JString("tag2")), JField("value", JString("tag2-value"))) :: - JObject(JField("key", JString("tag3")), JField("value", JString("tag3-value"))) :: - Nil - ) - } + val invalidSerialization = """[{"key" :"tag1"},{"key" :"tag2", "value":"tag2-value"},{"key" :"tag3", "value":"tag3-value"}]""" val duplicatedSerialization = @@ -75,22 +64,19 @@ class TagsTest extends Specification with Loggable { "Serializing and unserializing" should { "serialize in correct JSON" in { - JsonTagSerialisation.serializeTags(simpleTags) must - equalTo(jsonSimple) + simpleTags.toJson must equalTo(simpleSerialization) } "unserialize correct JSON" in { - CompleteJson.unserializeTags(simpleSerialization) must - equalTo(simpleTags) + simpleSerialization.fromJson[Tags] must beRight(simpleTags) } "fail to unserialize incorrect JSON" in { - CompleteJson.unserializeTags(invalidSerialization) must haveClass[Failure] + invalidSerialization.fromJson[Tags] must beLeft } "unserialize duplicated entries in JSON into uniques" in { - CompleteJson.unserializeTags(duplicatedSerialization) must - equalTo(simpleTags) + duplicatedSerialization.fromJson[Tags] must beRight(simpleTags) } } diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/NodeGroupForm.scala b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/NodeGroupForm.scala index c61fefb12f8..9d504200594 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/NodeGroupForm.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/NodeGroupForm.scala @@ -72,6 +72,7 @@ import org.apache.commons.text.StringEscapeUtils import scala.xml.* import zio.ZIO import zio.json.* +import zio.json.ast.* import zio.syntax.* object NodeGroupForm { @@ -276,8 +277,8 @@ class NodeGroupForm( _.name ) - private def showComplianceForGroup(progressBarSelector: String, optComplianceArray: Option[JsArray]) = { - val complianceHtml = optComplianceArray.map(js => s"buildComplianceBar(${js.toJsCmd})").getOrElse("\"No report\"") + private def showComplianceForGroup(progressBarSelector: String, optComplianceArray: Option[Json.Arr]) = { + val complianceHtml = optComplianceArray.map(js => s"buildComplianceBar(${js.toJson})").getOrElse("\"No report\"") Script(JsRaw(s"""$$("${progressBarSelector}").html(${complianceHtml});""")) } @@ -489,7 +490,7 @@ class NodeGroupForm( intro ++ tabProperties } - private def loadComplianceBar(isGlobalCompliance: Boolean): Option[JsArray] = { + private def loadComplianceBar(isGlobalCompliance: Boolean): Option[Json.Arr] = { val target = nodeGroup match { case Left(value) => value case Right(value) => GroupTarget(value.id) diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/RuleGrid.scala b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/RuleGrid.scala index 6e0e16eb6bd..455e7b93820 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/RuleGrid.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/RuleGrid.scala @@ -61,14 +61,8 @@ import net.liftweb.common.* import net.liftweb.http.* import net.liftweb.http.js.* import net.liftweb.http.js.JE.* -import net.liftweb.http.js.JE.AnonFunc import net.liftweb.http.js.JsCmds.* -import net.liftweb.json.JArray -import net.liftweb.json.JField -import net.liftweb.json.JObject -import net.liftweb.json.JsonAST.JValue -import net.liftweb.json.JsonParser -import net.liftweb.json.JString +import net.liftweb.json.* import net.liftweb.util.Helpers.* import org.apache.commons.text.StringEscapeUtils import org.joda.time.Interval @@ -741,8 +735,7 @@ object RuleGrid { val t5 = System.currentTimeMillis TimingDebugLogger.trace(s"Rule grid: transforming into data: get rule data: callback: ${t5 - t4}ms") - val tags = JsObj(line.rule.tags.map(tag => (tag.name.value, Str(tag.value.value))).toList*).toJsCmd - val tagsDisplayed = JsonTagSerialisation.serializeTags(line.rule.tags) + val tags = JsObj(line.rule.tags.map(tag => (tag.name.value, Str(tag.value.value))).toList*).toJsCmd RuleLine( line.rule.name, line.rule.id, @@ -757,7 +750,7 @@ object RuleGrid { line.policyMode, line.policyModeExplanation, tags, - tagsDisplayed + line.rule.tags ) } @@ -793,11 +786,20 @@ final case class RuleLine( policyMode: String, explanation: String, tags: String, - tagsDisplayed: JValue + tagsDisplayed: Tags ) extends JsTableLine { + private def serializeTags(tags: Tags): JValue = { + // sort all the tags by name + import net.liftweb.json.JsonDSL.* + val m: JValue = JArray( + tags.tags.toList.sortBy(_.name.value).map(t => ("key", t.name.value) ~ ("value", t.value.value)) + ) + m + } + /* Would love to have a reflexive way to generate that map ... */ - override def json(freshName: () => String): js.JsObj = { + override def json(freshName: () => String): JsObj = { val reasonField = reasons.map(r => ("reasons" -> escapeHTML(r))) @@ -818,7 +820,7 @@ final case class RuleLine( ("policyMode", policyMode), ("explanation", explanation), ("tags", tags), - ("tagsDisplayed", tagsDisplayed) + ("tagsDisplayed", serializeTags(tagsDisplayed)) ) base +* JsObj(optFields*) diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/TagsEditForm.scala b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/TagsEditForm.scala index 2d86ffbfd97..2ac19bbd255 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/TagsEditForm.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/TagsEditForm.scala @@ -1,8 +1,7 @@ package com.normation.rudder.web.components -import com.normation.rudder.domain.policies.JsonTagSerialisation +import com.normation.box.* import com.normation.rudder.domain.policies.Tags -import com.normation.rudder.repository.json.DataExtractor.CompleteJson import com.normation.rudder.web.ChooseTemplate import net.liftweb.common.* import net.liftweb.http.SHtml @@ -12,15 +11,16 @@ import net.liftweb.util.CssSel import net.liftweb.util.Helpers.* import org.apache.commons.text.StringEscapeUtils import scala.xml.NodeSeq +import zio.json.* class TagsEditForm(tags: Tags, objectId: String) extends Loggable { val templatePath: List[String] = List("templates-hidden", "components", "ComponentTags") def tagsTemplate: NodeSeq = ChooseTemplate(templatePath, "tag-form") - val jsTags: String = net.liftweb.json.compactRender(JsonTagSerialisation.serializeTags(tags)) + val jsTags: String = tags.toJson - def parseResult(s: String): Box[Tags] = CompleteJson.unserializeTags(s) + def parseResult(s: String): Box[Tags] = s.fromJson[Tags].toBox def tagsForm(controllerId: String, appId: String, update: Box[Tags] => Unit, isRule: Boolean): NodeSeq = { diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/AsyncComplianceService.scala b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/AsyncComplianceService.scala index 73b9fdfafa1..6258fb021db 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/AsyncComplianceService.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/AsyncComplianceService.scala @@ -159,7 +159,7 @@ class AsyncComplianceService( for { (key, optCompliance) <- compliances } yield { val value = kind.value(key) val displayCompliance = optCompliance - .map(_.toJsArray.toJsCmd) + .map(_.toJsArray.toJson) .getOrElse("""'
no data available
'""") s"${kind.jsContainer}['${value}'] = ${displayCompliance};" } diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/ComplianceData.scala b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/ComplianceData.scala index f9dfa5b0d40..f9188f88a67 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/ComplianceData.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/ComplianceData.scala @@ -45,10 +45,47 @@ import com.normation.rudder.domain.policies.* import com.normation.rudder.domain.reports.* import com.normation.rudder.facts.nodes.CoreNodeFact import com.normation.rudder.repository.FullActiveTechniqueCategory +import com.normation.rudder.web.services.EscapeHtml.* +import io.scalaland.chimney.* +import io.scalaland.chimney.syntax.* import net.liftweb.common.* import net.liftweb.http.* -import net.liftweb.http.js.JE.* -import net.liftweb.json.JsonAST.JValue +import org.apache.commons.text.StringEscapeUtils +import zio.json.* +import zio.json.internal.Write + +object EscapeHtml { + implicit class DoEscapeHtml(s: String) { + // this is needed because DataTable doesn't escape HTML element when using table.rows.add + def escapeHTML: String = StringEscapeUtils.escapeHtml4(s) + } +} + +/* + * This is used to provide the jsid: nextFuncName in main code, a decidable pseudo-random in tests. + */ +trait ProvideNextName { + def nextName: String +} + +object LiftProvideNextName extends ProvideNextName { + override def nextName: String = net.liftweb.util.Helpers.nextFuncName +} + +object ProvideNextName { + implicit val encoderProvideNextName: JsonEncoder[ProvideNextName] = JsonEncoder.string.contramap(_.nextName) +} + +final case class RuleComplianceLines(rules: List[RuleComplianceLine]) + +object RuleComplianceLines { + /* + * This is the main encoder that under the hood will transform to the JsonXXXLine corresponding object to encode them. + * It will need a `ProvideNextName` implicit in context to be used. + */ + implicit def encoderRuleComplianceLines(implicit next: ProvideNextName): JsonEncoder[RuleComplianceLines] = + JsonEncoder.list[JsonRuleComplianceLine].contramap[RuleComplianceLines](_.rules.map(_.transformInto[JsonRuleComplianceLine])) +} /* * That file contains all the datastructures related to @@ -73,29 +110,48 @@ final case class RuleComplianceLine( rule: Rule, id: RuleId, compliance: ComplianceLevel, - details: JsTableData[DirectiveComplianceLine], + details: List[DirectiveComplianceLine], policyMode: String, modeExplanation: String, - tags: JValue -) extends JsTableLine { - def json(freshName: () => String): js.JsObj = { - JsObj( - ("rule" -> escapeHTML(rule.name)), - ("compliance" -> jsCompliance(compliance)), - ("compliancePercent" -> compliance.computePercent().compliance), - ("id" -> escapeHTML(rule.id.serialize)), - ("details" -> details.json(freshName)), // unique id, usable as DOM id - rules, directives, etc can - // appear several time in a page - - ("jsid" -> freshName()), - ("isSystem" -> rule.isSystem), - ("policyMode" -> policyMode), - ("explanation" -> modeExplanation), - ("tags" -> tags) - ) + tags: Tags +) + +object RuleComplianceLine { + + implicit def transformRuleComplianceLine(implicit + next: ProvideNextName + ): Transformer[RuleComplianceLine, JsonRuleComplianceLine] = { + Transformer + .define[RuleComplianceLine, JsonRuleComplianceLine] + .withFieldComputed(_.rule, _.rule.name.escapeHTML) + .withFieldComputed(_.compliancePercent, _.compliance.computePercent().compliance) + .withFieldComputed(_.id, _.rule.id.serialize.escapeHTML) + .withFieldComputed(_.details, _.details.map(_.transformInto[JsonDirectiveComplianceLine])) + .withFieldConst(_.jsid, next) + .withFieldComputed(_.isSystem, _.rule.isSystem) + .withFieldRenamed(_.modeExplanation, _.explanation) + .buildTransformer } } +final case class JsonRuleComplianceLine( + rule: String, + compliance: ComplianceLevel, + compliancePercent: Double, + id: String, + details: List[JsonDirectiveComplianceLine], + jsid: ProvideNextName, + isSystem: Boolean, + policyMode: String, + explanation: String, + tags: Tags +) + +object JsonRuleComplianceLine { + import com.normation.rudder.domain.reports.ComplianceLevelSerialisation.array.* + implicit val encoderJsonRuleComplianceLine: JsonEncoder[JsonRuleComplianceLine] = DeriveJsonEncoder.gen +} + /* * Javascript object containing all data to create a line in the DataTable * { "directive" : Directive name [String] @@ -116,31 +172,50 @@ final case class DirectiveComplianceLine( techniqueName: String, techniqueVersion: TechniqueVersion, compliance: ComplianceLevel, - details: JsTableData[ComponentComplianceLine], + details: List[ComponentComplianceLine], policyMode: String, - modeExplanation: String, - tags: JValue -) extends JsTableLine { - override def json(freshName: () => String): js.JsObj = { - JsObj( - ("directive" -> escapeHTML(directive.name)), - ("id" -> escapeHTML(directive.id.uid.value)), - ("techniqueName" -> escapeHTML(techniqueName)), - ("techniqueVersion" -> escapeHTML(techniqueVersion.serialize)), - ("compliance" -> jsCompliance(compliance)), - ("compliancePercent" -> compliance.computePercent().compliance), - ("details" -> details.json(freshName)), // unique id, usable as DOM id - rules, directives, etc can - // appear several time in a page - - ("jsid" -> freshName()), - ("isSystem" -> directive.isSystem), - ("policyMode" -> policyMode), - ("explanation" -> modeExplanation), - ("tags" -> tags) - ) + explanation: String, + tags: Tags +) + +object DirectiveComplianceLine { + implicit def transformDirectiveComplianceLine(implicit + next: ProvideNextName + ): Transformer[DirectiveComplianceLine, JsonDirectiveComplianceLine] = { + Transformer + .define[DirectiveComplianceLine, JsonDirectiveComplianceLine] + .withFieldComputed(_.directive, _.directive.name.escapeHTML) + .withFieldComputed(_.id, _.directive.id.uid.value.escapeHTML) + .withFieldComputed(_.techniqueName, _.techniqueName.escapeHTML) + .withFieldComputed(_.techniqueVersion, _.techniqueVersion.serialize.escapeHTML) + .withFieldComputed(_.compliancePercent, _.compliance.computePercent().compliance) + .withFieldComputed(_.details, _.details.map(_.transformInto[JsonComponentComplianceLine])) + .withFieldConst(_.jsid, next) + .withFieldComputed(_.isSystem, _.directive.isSystem) + .buildTransformer } } +final case class JsonDirectiveComplianceLine( + directive: String, + id: String, + techniqueName: String, + techniqueVersion: String, + compliance: ComplianceLevel, + compliancePercent: Double, + details: List[JsonComponentComplianceLine], + jsid: ProvideNextName, + isSystem: Boolean, + policyMode: String, + explanation: String, + tags: Tags +) + +object JsonDirectiveComplianceLine { + import com.normation.rudder.domain.reports.ComplianceLevelSerialisation.array.* + implicit val encoderJsonDirectiveComplianceLine: JsonEncoder[JsonDirectiveComplianceLine] = DeriveJsonEncoder.gen +} + /* * Javascript object containing all data to create a line in the DataTable * { "component" : component name [String] @@ -153,51 +228,111 @@ final case class DirectiveComplianceLine( * } */ -sealed trait ComponentComplianceLine extends JsTableLine { +sealed trait ComponentComplianceLine { def component: String def compliance: ComplianceLevel } +object ComponentComplianceLine { + implicit def transformComponentComplianceLine(implicit + next: ProvideNextName + ): Transformer[ComponentComplianceLine, JsonComponentComplianceLine] = { + case x: BlockComplianceLine => x.transformInto[JsonBlockComplianceLine] + case x: ValueComplianceLine => x.transformInto[JsonValueComplianceLine] + } +} + final case class BlockComplianceLine( component: String, compliance: ComplianceLevel, - details: JsTableData[ComponentComplianceLine], + details: List[ComponentComplianceLine], reportingLogic: ReportingLogic -) extends ComponentComplianceLine { - def json(freshName: () => String): js.JsObj = { - JsObj( - ("component" -> escapeHTML(component)), - ("compliance" -> jsCompliance(compliance)), - ("compliancePercent" -> compliance.computePercent().compliance), - ("details" -> details.json(freshName)), - ("jsid" -> freshName()), - ("composition" -> reportingLogic.toString) - ) +) extends ComponentComplianceLine + +object BlockComplianceLine { + implicit def transformBlockComplianceLine(implicit + next: ProvideNextName + ): Transformer[BlockComplianceLine, JsonBlockComplianceLine] = { + Transformer + .define[BlockComplianceLine, JsonBlockComplianceLine] + .withFieldComputed(_.component, _.component.escapeHTML) + .withFieldComputed(_.compliancePercent, _.compliance.computePercent().compliance) + .withFieldComputed(_.details, _.details.map(_.transformInto[JsonComponentComplianceLine])) + .withFieldConst(_.jsid, next) + .withFieldComputed(_.composition, _.reportingLogic.toString) + .buildTransformer } + } final case class ValueComplianceLine( component: String, unexpanded: String, compliance: ComplianceLevel, - details: JsTableData[ComponentValueComplianceLine], + details: List[ComponentValueComplianceLine], noExpand: Boolean -) extends ComponentComplianceLine { - - def json(freshName: () => String): js.JsObj = { - JsObj( - ("component" -> escapeHTML(component)), - ("unexpanded" -> escapeHTML(unexpanded)), - ("compliance" -> jsCompliance(compliance)), - ("compliancePercent" -> compliance.computePercent().compliance), - ("details" -> details.json(freshName)), - ("noExpand" -> noExpand), - ("jsid" -> freshName()) - ) +) extends ComponentComplianceLine + +object ValueComplianceLine { + implicit def transformValueComplianceLine(implicit + next: ProvideNextName + ): Transformer[ValueComplianceLine, JsonValueComplianceLine] = { + Transformer + .define[ValueComplianceLine, JsonValueComplianceLine] + .withFieldComputed(_.component, _.component.escapeHTML) + .withFieldComputed(_.unexpanded, _.unexpanded.escapeHTML) + .withFieldComputed(_.compliancePercent, _.compliance.computePercent().compliance) + .withFieldComputed(_.details, _.details.map(_.transformInto[JsonComponentValueComplianceLine])) + .withFieldConst(_.jsid, next) + .buildTransformer } } +sealed trait JsonComponentComplianceLine + +object JsonComponentComplianceLine { + implicit val encoderJsonComponentComplianceLine: JsonEncoder[JsonComponentComplianceLine] = { + new JsonEncoder[JsonComponentComplianceLine] { + override def unsafeEncode(a: JsonComponentComplianceLine, indent: Option[Int], out: Write): Unit = { + a match { + case x: JsonBlockComplianceLine => JsonEncoder[JsonBlockComplianceLine].unsafeEncode(x, indent, out) + case x: JsonValueComplianceLine => JsonEncoder[JsonValueComplianceLine].unsafeEncode(x, indent, out) + } + } + } + } +} + +final case class JsonBlockComplianceLine( + component: String, + compliance: ComplianceLevel, + compliancePercent: Double, + details: List[JsonComponentComplianceLine], + jsid: ProvideNextName, + composition: String +) extends JsonComponentComplianceLine + +object JsonBlockComplianceLine { + import com.normation.rudder.domain.reports.ComplianceLevelSerialisation.array.* + implicit val encoderJsonBlockComplianceLine: JsonEncoder[JsonBlockComplianceLine] = DeriveJsonEncoder.gen +} + +final case class JsonValueComplianceLine( + component: String, + unexpanded: String, + compliance: ComplianceLevel, + compliancePercent: Double, + details: List[JsonComponentValueComplianceLine], + noExpand: Boolean, + jsid: ProvideNextName +) extends JsonComponentComplianceLine + +object JsonValueComplianceLine { + import com.normation.rudder.domain.reports.ComplianceLevelSerialisation.array.* + implicit val encoderJsonValueComplianceLine: JsonEncoder[JsonValueComplianceLine] = DeriveJsonEncoder.gen +} + /* * Javascript object containing all data to create a line in the DataTable * { "value" : value of the key [String] @@ -216,25 +351,40 @@ final case class ComponentValueComplianceLine( compliance: ComplianceLevel, status: String, statusClass: String -) extends JsTableLine { - - def json(freshName: () => String): js.JsObj = { - JsObj( - ("value" -> escapeHTML(value)), - ("unexpanded" -> escapeHTML(unexpandedValue)), - ("status" -> status), - ("statusClass" -> statusClass), - ("messages" -> JsArray(messages.map { case (s, m) => JsObj(("status" -> s), ("value" -> escapeHTML(m))) })), - ("compliance" -> jsCompliance(compliance)), - ("compliancePercent" -> compliance.computePercent().compliance), // unique id, usable as DOM id - rules, directives, etc can - // appear several time in a page - - ("jsid" -> freshName()) - ) +) + +object ComponentValueComplianceLine { + implicit def transformComponentValueComplianceLine(implicit + next: ProvideNextName + ): Transformer[ComponentValueComplianceLine, JsonComponentValueComplianceLine] = { + Transformer + .define[ComponentValueComplianceLine, JsonComponentValueComplianceLine] + .withFieldComputed(_.value, _.value.escapeHTML) + .withFieldComputed(_.unexpanded, _.unexpandedValue.escapeHTML) + .withFieldComputed(_.messages, _.messages.map { case (s, v) => Map("status" -> s, "value" -> v.escapeHTML) }) + .withFieldComputed(_.compliancePercent, _.compliance.computePercent().compliance) + .withFieldConst(_.jsid, next) + .buildTransformer } } +final case class JsonComponentValueComplianceLine( + value: String, + unexpanded: String, + status: String, + statusClass: String, + messages: List[Map[String, String]], + compliance: ComplianceLevel, + compliancePercent: Double, + jsid: ProvideNextName +) + +object JsonComponentValueComplianceLine { + import com.normation.rudder.domain.reports.ComplianceLevelSerialisation.array.* + implicit val encoderJsonComponentValueComplianceLine: JsonEncoder[JsonComponentValueComplianceLine] = DeriveJsonEncoder.gen +} + object ComplianceData extends Loggable { /* @@ -252,7 +402,7 @@ object ComplianceData extends Loggable { rules: Seq[Rule], globalMode: GlobalPolicyMode, addOverridden: Boolean - ): JsTableData[RuleComplianceLine] = { + ): RuleComplianceLines = { // add overridden directive in the list under there rule val overridesByRules = if (addOverridden) { @@ -287,13 +437,13 @@ object ComplianceData extends Loggable { rule, rule.id, aggregate.compliance, - JsTableData(details), + details, policyMode, explanation, - JsonTagSerialisation.serializeTags(rule.tags) + rule.tags ) } - JsTableData(ruleComplianceLine.toList.sortBy(_.id.serialize)) + RuleComplianceLines(ruleComplianceLine.toList.sortBy(_.id.serialize)) } private def getOverriddenDirectiveDetails( @@ -330,10 +480,10 @@ object ComplianceData extends Loggable { overriddenTechName, overriddenTechVersion, ComplianceLevel(), - JsTableData(Nil), + Nil, policyMode, explanation, - JsonTagSerialisation.serializeTags(overriddenDir.tags) + overriddenDir.tags ) } @@ -357,7 +507,6 @@ object ComplianceData extends Loggable { val techniqueVersion = directive.techniqueVersion val components = getComponentsComplianceDetails(directiveStatus.components, includeMessage = true) val (policyMode, explanation) = computeMode(directive.policyMode) - val directiveTags = JsonTagSerialisation.serializeTags(directive.tags) DirectiveComplianceLine( directive, techniqueName, @@ -366,7 +515,7 @@ object ComplianceData extends Loggable { components, policyMode, explanation, - directiveTags + directive.tags ) } @@ -378,7 +527,7 @@ object ComplianceData extends Loggable { private def getComponentsComplianceDetails( components: List[ComponentStatusReport], includeMessage: Boolean - ): JsTableData[ComponentComplianceLine] = { + ): List[ComponentComplianceLine] = { val componentsComplianceData = components.map { case component: BlockStatusReport => BlockComplianceLine( @@ -405,7 +554,7 @@ object ComplianceData extends Loggable { ) } - JsTableData(componentsComplianceData) + componentsComplianceData } //////////////// Value Report /////////////// @@ -413,7 +562,7 @@ object ComplianceData extends Loggable { // From Node Point of view private def getValuesComplianceDetails( values: List[ComponentValueStatusReport] - ): JsTableData[ComponentValueComplianceLine] = { + ): List[ComponentValueComplianceLine] = { val valuesComplianceData = for { value <- values } yield { @@ -431,7 +580,7 @@ object ComplianceData extends Loggable { severity ) } - JsTableData(valuesComplianceData) + valuesComplianceData } private def getDisplayStatusFromSeverity(severity: String): String = { diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/ReportDisplayer.scala b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/ReportDisplayer.scala index 4504870fdca..a55d47689b2 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/ReportDisplayer.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/ReportDisplayer.scala @@ -66,6 +66,7 @@ import net.liftweb.util.Helpers.* import org.joda.time.DateTime import scala.xml.NodeSeq import scala.xml.NodeSeq.seqToNodeSeq +import zio.json.* /** * Display the last reports of a server @@ -136,7 +137,8 @@ class ReportDisplayer( addOverridden: Boolean, defaultRunInterval: Int ): AnonFunc = { - def refreshData: Box[JsCmd] = { + implicit val next: ProvideNextName = LiftProvideNextName + def refreshData: Box[JsCmd] = { for { report <- getReports(node.id) data <- getComplianceData(node.id, report, addOverridden) @@ -145,7 +147,7 @@ class ReportDisplayer( import net.liftweb.util.Helpers.encJs val intro = encJs(displayIntro(report, node.rudderSettings, defaultRunInterval).toString) JsRaw( - s"""refreshTable("${tableId}",${data}); $$("#node-compliance-intro").replaceWith(${intro})""" + s"""refreshTable("${tableId}",${data.toJson}); $$("#node-compliance-intro").replaceWith(${intro})""" ) // JsRaw ok, escaped } } @@ -577,7 +579,7 @@ class ReportDisplayer( nodeId: NodeId, reportStatus: NodeStatusReport, addOverridden: Boolean - ): Box[JsTableData[RuleComplianceLine]] = { + ): Box[RuleComplianceLines] = { for { directiveLib <- directiveRepository.getFullDirectiveLibrary().toBox allNodeInfos <- nodeFactRepo.getAll()(CurrentUser.queryContext).toBox diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/snippet/HomePage.scala b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/snippet/HomePage.scala index 6a82e678068..f9c6e553698 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/snippet/HomePage.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/snippet/HomePage.scala @@ -68,6 +68,7 @@ import net.liftweb.http.js.JE.* import net.liftweb.http.js.JsCmds.* import scala.collection.MapView import scala.xml.* +import zio.json.* case class ScoreChart(scoreValue: ScoreValue, value: Int, noScoreLegend: Option[String]) { import com.normation.rudder.score.ScoreValue.* @@ -322,7 +323,7 @@ class HomePage extends StatefulSnippet { import com.normation.rudder.domain.reports.ComplianceLevelSerialisation.* (bar.copy(pending = 0).toJsArray, value) case None => - (JsArray(Nil), -1L) + (ast.Json.Arr(), -1L) } val n4 = System.currentTimeMillis @@ -330,7 +331,7 @@ class HomePage extends StatefulSnippet { Script(OnLoad(JsRaw(s""" homePage( - ${complianceBar.toJsCmd} + ${complianceBar.toJson} , ${globalCompliance} , ${data.toJsCmd} , ${pendingNodes.toJsCmd} diff --git a/webapp/sources/rudder/rudder-web/src/test/scala/com/normation/rudder/web/services/ComplianceLineTest.scala b/webapp/sources/rudder/rudder-web/src/test/scala/com/normation/rudder/web/services/ComplianceLineTest.scala index c8c87e8ef05..f65e91e9dd6 100644 --- a/webapp/sources/rudder/rudder-web/src/test/scala/com/normation/rudder/web/services/ComplianceLineTest.scala +++ b/webapp/sources/rudder/rudder-web/src/test/scala/com/normation/rudder/web/services/ComplianceLineTest.scala @@ -56,6 +56,7 @@ import org.specs2.mutable.* import org.specs2.runner.JUnitRunner import scala.collection.MapView import scala.util.Random +import zio.json.* @RunWith(classOf[JUnitRunner]) class ComplianceLineTest extends Specification with JsonSpecMatcher { @@ -66,8 +67,10 @@ class ComplianceLineTest extends Specification with JsonSpecMatcher { val nodes: MapView[NodeId, CoreNodeFact] = mockCompliance.nodeFactRepo.getAll().runNow val directives: FullActiveTechniqueCategory = mockDirectives.directiveRepo.getFullDirectiveLibrary().runNow - val stableRandom = new Random(42) - def freshName(): String = stableRandom.nextLong().toString + implicit object StableRandom extends ProvideNextName { + val stableRandom = new Random(42) + override def nextName: String = stableRandom.nextLong().toString + } "compliance lines serialisation" >> { val nodeId = NodeId("n1") @@ -83,8 +86,7 @@ class ComplianceLineTest extends Specification with JsonSpecMatcher { GlobalPolicyMode(Enforce, PolicyModeOverrides.Always), true ) - .json(freshName _) - .toJsCmd + .toJson lines must equalsJsonSemantic( """[ @@ -125,14 +127,14 @@ class ComplianceLineTest extends Specification with JsonSpecMatcher { | "compliance":[[0,0.0],[0,0.0], [1, 100.0], [0,0.0],[0,0.0],[0,0.0],[0,0.0],[0,0.0],[0,0.0],[0,0.0],[0,0.0],[0,0.0],[0,0.0],[0,0.0] | ], | "compliancePercent":100.0, - | "jsid":"-5025562857975149833" + | "jsid":"-5843495416241995736" | } | ], | "noExpand":false, - | "jsid":"-5843495416241995736" + | "jsid":"5111195811822994797" | } | ], - | "jsid":"5694868678511409995", + | "jsid":"-1782466964123969572", | "isSystem":false, | "policyMode":"audit", | "explanation":"The Node is configured to enforce but is overridden to audit by this Directive. ", @@ -144,7 +146,7 @@ class ComplianceLineTest extends Specification with JsonSpecMatcher { | ] | } | ], - | "jsid":"5111195811822994797", + | "jsid":"5086654115216342560", | "isSystem":false, | "policyMode":"audit", | "explanation":"The Node is configured to enforce but is overridden to audit by all Directives. ", @@ -187,21 +189,21 @@ class ComplianceLineTest extends Specification with JsonSpecMatcher { | "compliance":[[0,0.0],[0,0.0],[0,0.0], [1, 100.0], [0,0.0],[0,0.0],[0,0.0],[0,0.0],[0,0.0],[0,0.0],[0,0.0],[0,0.0],[0,0.0],[0,0.0] | ], | "compliancePercent":100.0, - | "jsid":"-6169532649852302182" + | "jsid":"-4004755535478349341" | } | ], | "noExpand":false, - | "jsid":"-1782466964123969572" + | "jsid":"8051837266862454915" | } | ], - | "jsid":"6802844026563419272", + | "jsid":"7130900098642117381", | "isSystem":false, | "policyMode":"enforce", | "explanation":"Enforce is forced by this Node mode", | "tags":[] | } | ], - | "jsid":"5086654115216342560", + | "jsid":"-7482923245497525943", | "isSystem":false, | "policyMode":"enforce", | "explanation":"enforce mode is forced by this Node", @@ -244,21 +246,21 @@ class ComplianceLineTest extends Specification with JsonSpecMatcher { | "compliance":[[0,0.0],[0,0.0],[0,0.0],[0,0.0], [1, 100.0], [0,0.0],[0,0.0],[0,0.0],[0,0.0],[0,0.0],[0,0.0],[0,0.0],[0,0.0],[0,0.0] | ], | "compliancePercent":0.0, - | "jsid":"8552898714322622292" + | "jsid":"-3210362905434573697" | } | ], | "noExpand":false, - | "jsid":"-4004755535478349341" + | "jsid":"-7610621359446545191" | } | ], - | "jsid":"-1488139573943419793", + | "jsid":"-7912908803613548926", | "isSystem":false, | "policyMode":"enforce", | "explanation":"Enforce is forced by this Node mode", | "tags":[] | } | ], - | "jsid":"8051837266862454915", + | "jsid":"-4565385657661118002", | "isSystem":false, | "policyMode":"enforce", | "explanation":"enforce mode is forced by this Node",