Skip to content

Commit

Permalink
Add cats-xml module (#129)
Browse files Browse the repository at this point in the history
* Add cats-xml module
  • Loading branch information
geirolz authored Dec 26, 2022
1 parent 9a3b874 commit f9e1c41
Show file tree
Hide file tree
Showing 13 changed files with 517 additions and 18 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ eRules supports Scala 2.13 and 3

**Sbt**
```sbt
libraryDependencies += "com.github.geirolz" %% "erules-core" % "0.0.6"
libraryDependencies += "com.github.geirolz" %% "erules-core" % "0.0.7"
```


Expand Down Expand Up @@ -79,7 +79,7 @@ val allPersonRules: NonEmptyList[Rule[Id, Person]] = NonEmptyList.of(
.targetInfo("age")
.contramap(_.age)
)
// allPersonRules: NonEmptyList[Rule[Id, Person]] = NonEmptyList(RuleImpl(scala.Function1$$Lambda$10486/0x00000008029eb390@3b64b963,Check UK citizenship,None,Some(citizenship)), RuleImpl(scala.Function1$$Lambda$10486/0x00000008029eb390@65d4d410,Check Age >= 18,None,Some(age)))
// allPersonRules: NonEmptyList[Rule[Id, Person]] = NonEmptyList(RuleImpl(scala.Function1$$Lambda$12894/0x0000000802cbcc38@1cf1a60b,Check UK citizenship,None,Some(citizenship)), RuleImpl(scala.Function1$$Lambda$12894/0x0000000802cbcc38@2df2b532,Check Age >= 18,None,Some(age)))
```

N.B. Importing even the `erules-generic` you can use macro to auto-generate the target info using `contramapTarget` method.
Expand Down Expand Up @@ -119,7 +119,7 @@ result.unsafeRunSync().asReport[String]
// - Rule: Check UK citizenship
// - Description:
// - Target: citizenship
// - Execution time: 123167 nanoseconds
// - Execution time: 100375 nanoseconds
//
// - Verdict: Right(Deny)
// - Because: Only UK citizenship is allowed!
Expand All @@ -128,7 +128,7 @@ result.unsafeRunSync().asReport[String]
// - Rule: Check Age >= 18
// - Description:
// - Target: age
// - Execution time: 16709 nanoseconds
// - Execution time: 10375 nanoseconds
//
// - Verdict: Right(Deny)
// - Because: Only >= 18 age are allowed!
Expand All @@ -142,4 +142,5 @@ result.unsafeRunSync().asReport[String]
### Modules
- [erules-generic](https://github.com/geirolz/erules/tree/main/modules/generic)
- [erules-circe](https://github.com/geirolz/erules/tree/main/modules/circe)
- [erules-cats-xml](https://github.com/geirolz/erules/tree/main/modules/cats-xml)
- [erules-scalatest](https://github.com/geirolz/erules/tree/main/modules/scalatest)
16 changes: 13 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ lazy val erules: Project = project
)
)
)
.aggregate(core, generic, circe, scalatest)
.aggregate(core, generic, circe, `cats-xml`, scalatest)

lazy val core: Project =
buildModule(
Expand Down Expand Up @@ -70,6 +70,16 @@ lazy val circe: Project =
libraryDependencies ++= ProjectDependencies.Circe.dedicated
)

lazy val `cats-xml`: Project =
buildModule(
prjModuleName = "cats-xml",
toPublish = true,
folder = "modules"
).dependsOn(core)
.settings(
libraryDependencies ++= ProjectDependencies.CatsXml.dedicated
)

lazy val scalatest: Project =
buildModule(
prjModuleName = "scalatest",
Expand All @@ -83,12 +93,11 @@ lazy val scalatest: Project =
//=============================== MODULES UTILS ===============================
def buildModule(prjModuleName: String, toPublish: Boolean, folder: String): Project = {
val keys = prjModuleName.split("-")
val id = keys.reduce(_ + _.capitalize)
val docName = keys.mkString(" ")
val prjFile = file(s"$folder/$prjModuleName")
val docNameStr = s"$prjName $docName"

Project(id, prjFile)
Project(prjModuleName, prjFile)
.settings(baseSettings)
.settings(
description := moduleName.value,
Expand Down Expand Up @@ -216,6 +225,7 @@ addCommandAlias(
core,
generic,
circe,
`cats-xml`,
scalatest
).map(prj => s"project ${prj.id}-docs; mdoc").mkString(";") + s";project $prjName;"
)
1 change: 1 addition & 0 deletions core/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,5 @@ result.unsafeRunSync().asReport[String]
### Modules
- [erules-generic](https://github.com/geirolz/erules/tree/main/modules/generic)
- [erules-circe](https://github.com/geirolz/erules/tree/main/modules/circe)
- [erules-cats-xml](https://github.com/geirolz/erules/tree/main/modules/cats-xml)
- [erules-scalatest](https://github.com/geirolz/erules/tree/main/modules/scalatest)
147 changes: 147 additions & 0 deletions modules/cats-xml/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Erules Cats Xml
The purpose of this module is to provide `Encoder` instances of `erules` types
and the `XmlReportEncoder` instances to produce an XML report.

**Sbt**
```sbt
libraryDependencies += "com.github.geirolz" %% "erules-core" % "0.0.7"
libraryDependencies += "com.github.geirolz" %% "erules-cats-xml" % "0.0.7"
```

### Usage

Given these data classes
```scala
case class Country(value: String)
case class Age(value: Int)

case class Citizenship(country: Country)
case class Person(
name: String,
lastName: String,
age: Age,
citizenship: Citizenship
)
```

Let's write the rules!
```scala
import erules.core.Rule
import erules.core.RuleVerdict.*
import cats.data.NonEmptyList
import cats.Id

val checkCitizenship: Rule[Id, Citizenship] =
Rule("Check UK citizenship").apply[Id, Citizenship]{
case Citizenship(Country("UK")) => Allow.withoutReasons
case _ => Deny.because("Only UK citizenship is allowed!")
}
// checkCitizenship: Rule[Id, Citizenship] = RuleImpl(<function1>,Check UK citizenship,None,None)

val checkAdultAge: Rule[Id, Age] =
Rule("Check Age >= 18").apply[Id, Age] {
case a: Age if a.value >= 18 => Allow.withoutReasons
case _ => Deny.because("Only >= 18 age are allowed!")
}
// checkAdultAge: Rule[Id, Age] = RuleImpl(<function1>,Check Age >= 18,None,None)

val allPersonRules: NonEmptyList[Rule[Id, Person]] = NonEmptyList.of(
checkCitizenship
.targetInfo("citizenship")
.contramap(_.citizenship),
checkAdultAge
.targetInfo("age")
.contramap(_.age)
)
// allPersonRules: NonEmptyList[Rule[Id, Person]] = NonEmptyList(RuleImpl(scala.Function1$$Lambda$12894/0x0000000802cbcc38@791d9aef,Check UK citizenship,None,Some(citizenship)), RuleImpl(scala.Function1$$Lambda$12894/0x0000000802cbcc38@4c8568e9,Check Age >= 18,None,Some(age)))
```

Import
```scala
import erules.cats.xml.implicits.*
```

Define the `Person` encoder
```scala
import cats.xml.codec.Encoder
import cats.xml.XmlNode
import cats.xml.implicits.*

implicit val personEncoder: Encoder[Person] = Encoder.of(person =>
XmlNode("Person")
.withAttributes(
"name" := person.name,
"lastName" := person.lastName,
"age" := person.age.value
)
.withChildren(
XmlNode("Citizenship")
.withAttributes(
"country" := person.citizenship.country.value
)
)
)
```

And create the JSON report
```scala
import erules.core.*
import erules.implicits.*
import erules.cats.xml.implicits.*

import cats.effect.IO
import cats.effect.unsafe.implicits.*

val person: Person = Person("Mimmo", "Rossi", Age(16), Citizenship(Country("IT")))
// person: Person = Person(Mimmo,Rossi,Age(16),Citizenship(Country(IT)))

val result: IO[EngineResult[Person]] = for {
engine <- RulesEngine[IO].withRules[Id, Person](allPersonRules).denyAllNotAllowed
result <- engine.parEval(person)
} yield result
// result: IO[EngineResult[Person]] = IO(...)

//yolo
result.unsafeRunSync().asXmlReport
// res0: cats.xml.Xml = <EngineResult>
// <Data>
// <Person name="Mimmo" lastName="Rossi" age="16">
// <Citizenship country="IT"/>
// </Person>
// </Data>
// <Verdict type="Denied">
// <EvaluatedRules>
// <RuleResult>
// <Rule name="Check UK citizenship" description="" targetInfo="citizenship">
// <FullDescription>
// Check UK citizenship for citizenship
// </FullDescription>
// </Rule>
// <Verdict type="Deny">
// <Reasons>
// <Reason>
// Only UK citizenship is allowed!
// </Reason>
// </Reasons>
// </Verdict>
// <ExecutionTime>
// <Duration length="134583" unit="NANOSECONDS"/>
// </ExecutionTime>
// </RuleResult>
// <RuleResult>
// <Rule name="Check Age >= 18" description="" targetInfo="age">
// <FullDescription>Check Age >= 18 for age</FullDescription>
// </Rule>
// <Verdict type="Deny">
// <Reasons>
// <Reason>Only >= 18 age are allowed!</Reason>
// </Reasons>
// </Verdict>
// <ExecutionTime>
// <Duration length="8833" unit="NANOSECONDS"/>
// </ExecutionTime>
// </RuleResult>
// </EvaluatedRules>
// </Verdict>
// </EngineResult>
```
101 changes: 101 additions & 0 deletions modules/cats-xml/docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Erules Cats Xml
The purpose of this module is to provide `Encoder` instances of `erules` types
and the `XmlReportEncoder` instances to produce an XML report.

**Sbt**
```sbt
libraryDependencies += "com.github.geirolz" %% "erules-core" % "@VERSION@"
libraryDependencies += "com.github.geirolz" %% "erules-cats-xml" % "@VERSION@"
```

### Usage

Given these data classes
```scala mdoc:to-string
case class Country(value: String)
case class Age(value: Int)

case class Citizenship(country: Country)
case class Person(
name: String,
lastName: String,
age: Age,
citizenship: Citizenship
)
```

Let's write the rules!
```scala mdoc:to-string
import erules.core.Rule
import erules.core.RuleVerdict.*
import cats.data.NonEmptyList
import cats.Id

val checkCitizenship: Rule[Id, Citizenship] =
Rule("Check UK citizenship").apply[Id, Citizenship]{
case Citizenship(Country("UK")) => Allow.withoutReasons
case _ => Deny.because("Only UK citizenship is allowed!")
}

val checkAdultAge: Rule[Id, Age] =
Rule("Check Age >= 18").apply[Id, Age] {
case a: Age if a.value >= 18 => Allow.withoutReasons
case _ => Deny.because("Only >= 18 age are allowed!")
}

val allPersonRules: NonEmptyList[Rule[Id, Person]] = NonEmptyList.of(
checkCitizenship
.targetInfo("citizenship")
.contramap(_.citizenship),
checkAdultAge
.targetInfo("age")
.contramap(_.age)
)
```

Import
```scala mdoc:silent
import erules.cats.xml.implicits.*
```

Define the `Person` encoder
```scala mdoc:silent
import cats.xml.codec.Encoder
import cats.xml.XmlNode
import cats.xml.implicits.*

implicit val personEncoder: Encoder[Person] = Encoder.of(person =>
XmlNode("Person")
.withAttributes(
"name" := person.name,
"lastName" := person.lastName,
"age" := person.age.value
)
.withChildren(
XmlNode("Citizenship")
.withAttributes(
"country" := person.citizenship.country.value
)
)
)
```

And create the JSON report
```scala mdoc:to-string
import erules.core.*
import erules.implicits.*
import erules.cats.xml.implicits.*

import cats.effect.IO
import cats.effect.unsafe.implicits.*

val person: Person = Person("Mimmo", "Rossi", Age(16), Citizenship(Country("IT")))

val result: IO[EngineResult[Person]] = for {
engine <- RulesEngine[IO].withRules[Id, Person](allPersonRules).denyAllNotAllowed
result <- engine.parEval(person)
} yield result

//yolo
result.unsafeRunSync().asXmlReport
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package erules.cats.xml

import cats.xml.codec.Encoder
import cats.xml.XmlNode

import scala.concurrent.duration.FiniteDuration

private[xml] object GenericCatsXmlInstances extends GenericCatsXmlEncoderInstances
private[xml] sealed trait GenericCatsXmlEncoderInstances {

import cats.xml.syntax.*

implicit final val finiteDurationEncoder: Encoder[FiniteDuration] =
Encoder.of(a =>
XmlNode("Duration").withAttributes(
"length" := a.length,
"unit" := a.unit.name
)
)

implicit def eitherEncoder[A, B](implicit a: Encoder[A], b: Encoder[B]): Encoder[Either[A, B]] =
Encoder.of {
case Left(v) => v.toXml
case Right(v) => v.toXml
}

implicit val throwableEncoder: Encoder[Throwable] =
Encoder.of(ex =>
XmlNode("Throwable").withAttributes(
"message" := ex.getMessage,
"causeMessage" := Option(ex.getCause).map(_.getMessage).getOrElse("")
)
)
}
Loading

0 comments on commit f9e1c41

Please sign in to comment.