Skip to content

Commit

Permalink
add serialization and rule generation section
Browse files Browse the repository at this point in the history
  • Loading branch information
cottand committed Jun 3, 2022
1 parent f2b6bf1 commit 1145147
Show file tree
Hide file tree
Showing 13 changed files with 252 additions and 37 deletions.
3 changes: 3 additions & 0 deletions lib/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
plugins {
kotlin("jvm")
kotlin("plugin.serialization") version "1.6.21"
// id("org.jlleitschuh.gradle.ktlint") version "10.2.1"
`java-library`
}

dependencies {

implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation("org.jeasy:easy-rules-core:4.1.0") {
exclude(group = "org.slf4j")
}
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3")

testImplementation("org.slf4j:slf4j-log4j12:1.7.29")
}
28 changes: 28 additions & 0 deletions lib/src/main/kotlin/eu/dcotta/confis/model/CircumstanceMap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.plus
import kotlinx.collections.immutable.toPersistentList
import kotlinx.collections.immutable.toPersistentMap
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

/**
* A [CircumstanceMap] functions a lot like a CoroutineContext: it is a collection of [Circumstance]s that can be
Expand All @@ -18,6 +25,7 @@ import kotlinx.collections.immutable.toPersistentMap
* uniqueness of their [Circumstance.Key], which is what uniquely identifies a [Circumstance] within a [CircumstanceMap]
* (and what keeps track of its type).
*/
@Serializable(with = CircumstanceMap.Serializer::class)
class CircumstanceMap private constructor(
private val map: PersistentMap<Key<*>, Circumstance>,
) {
Expand Down Expand Up @@ -88,4 +96,24 @@ class CircumstanceMap private constructor(
val empty = CircumstanceMap(persistentMapOf())
fun of(vararg elems: Circumstance) = CircumstanceMap(elems.associateBy { it.key }.toPersistentMap())
}

object Serializer : KSerializer<CircumstanceMap> {

private val mapSerializer: KSerializer<Map<Key<Circumstance>, Circumstance>> =
MapSerializer(Key.serializer(Circumstance.serializer()), Circumstance.serializer())

@Suppress("OPT_IN_IS_NOT_ENABLED")
@OptIn(ExperimentalSerializationApi::class)
override val descriptor: SerialDescriptor =
SerialDescriptor("CircumstanceMap", mapSerializer.descriptor)

override fun deserialize(decoder: Decoder): CircumstanceMap {
val decoded = decoder.decodeSerializableValue(mapSerializer)
return CircumstanceMap(decoded.toPersistentMap())
}

override fun serialize(encoder: Encoder, value: CircumstanceMap) {
encoder.encodeSerializableValue(mapSerializer, value.map)
}
}
}
8 changes: 8 additions & 0 deletions lib/src/main/kotlin/eu/dcotta/confis/model/agreement.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package eu.dcotta.confis.model

import eu.dcotta.confis.dsl.AgreementBuilder
import kotlinx.collections.immutable.toPersistentSet
import kotlinx.serialization.Serializable

@Serializable
data class Agreement(
val clauses: List<Clause>,
val parties: List<Party>,
Expand All @@ -17,10 +19,13 @@ fun Agreement(builder: AgreementBuilder.() -> Unit): Agreement = AgreementBuilde

sealed interface NoCircumstance

@Serializable
sealed interface Clause {
@JvmInline
@Serializable
value class Text(val string: String) : Clause, NoCircumstance

@Serializable
data class PermissionWithCircumstances(
val permission: Permission,
val circumstanceAllowance: Allowance,
Expand All @@ -29,11 +34,13 @@ sealed interface Clause {
val sentence by permission::sentence
}

@Serializable
data class RequirementWithCircumstances(
val sentence: Sentence,
val circumstances: CircumstanceMap,
) : Clause

@Serializable
data class Requirement(val sentence: Sentence) : Clause, NoCircumstance {
val subject by sentence::subject
val obj by sentence::obj
Expand All @@ -42,6 +49,7 @@ sealed interface Clause {
override fun toString() = "Requirement($sentence)"
}

@Serializable
data class Permission(val allowance: Allowance, val sentence: Sentence) : Clause, NoCircumstance {
val subject by sentence::subject
val obj by sentence::obj
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import eu.dcotta.confis.model.Party
import eu.dcotta.confis.model.Sentence
import eu.dcotta.confis.model.circumstance.Circumstance.Key
import eu.dcotta.confis.model.circumstance.Circumstance.SetKey
import kotlinx.serialization.Serializable

@Serializable
data class Consent(val sentence: Sentence, val from: Party) : Circumstance {

override fun generalises(other: Circumstance): Boolean = other is Consent &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package eu.dcotta.confis.model.circumstance

import eu.dcotta.confis.model.Sentence
import eu.dcotta.confis.model.circumstance.Circumstance.SetKey
import kotlinx.serialization.Serializable

/**
* A [Circumstance] that represents a [Sentence] that may have happened in the past
* with respect to its corresponding clause
*/
@Serializable
data class PrecedentSentence(val sentence: Sentence) : Circumstance {

override val key get() = Key(sentence)
Expand All @@ -20,6 +22,7 @@ data class PrecedentSentence(val sentence: Sentence) : Circumstance {
}

@JvmInline
@Serializable
value class Key(val sentence: Sentence) : Circumstance.Key<PrecedentSentence>

companion object KeySet : SetKey<PrecedentSentence> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package eu.dcotta.confis.model.circumstance

import eu.dcotta.confis.model.CircumstanceMap
import kotlinx.serialization.Serializable

interface Circumstance {
@Serializable
sealed interface Circumstance {

/**
* whether given [other] circumstances, are we in this [Circumstance].
Expand All @@ -11,12 +13,14 @@ interface Circumstance {
*/
infix fun generalises(other: Circumstance): Boolean

interface Key<T : Circumstance>
@Serializable
sealed interface Key<out T : Circumstance>

interface SetKey<T : Circumstance> {
fun Key<*>.fromSetOrNull(): Key<T>?
}

@Serializable
val key: Key<*>

fun render(): String = toString()
Expand All @@ -34,8 +38,13 @@ infix fun Circumstance.disjoint(other: Circumstance) = when (this) {
*/
infix fun Circumstance.generalises(map: CircumstanceMap) = map[key]?.let { generalises(it) } ?: false

interface OverlappingCircumstance : Circumstance {
@Serializable
sealed interface OverlappingCircumstance : Circumstance {
infix fun overlapsWith(other: Circumstance): Boolean
}

operator fun Circumstance.plus(other: Circumstance) = CircumstanceMap.of(this, other)

interface TestCircumstance : Circumstance {
interface Key<T : TestCircumstance> : Circumstance.Key<T>
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package eu.dcotta.confis.model.circumstance

import eu.dcotta.confis.model.circumstance.Circumstance.Key
import kotlinx.serialization.Serializable

@Serializable
enum class Purpose {
Commercial, Research, Internal;
}

@Serializable
data class PurposePolicy(val purposes: List<Purpose>) : Circumstance {
constructor(vararg purposes: Purpose) : this(purposes.asList())

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
package eu.dcotta.confis.model.circumstance

import eu.dcotta.confis.model.circumstance.TimeRange.Range
import kotlinx.serialization.Serializable
import java.text.SimpleDateFormat
import java.util.Calendar

sealed interface TimeRange : OverlappingCircumstance {

data class Range(override val start: Date, override val endInclusive: Date) : ClosedRange<Date>, TimeRange {
@Serializable
data class Range(override val start: Date, override val endInclusive: Date) :
ClosedRange<Date>,
TimeRange,
Circumstance,
OverlappingCircumstance {
override fun generalises(other: Circumstance) = other is TimeRange && when (other) {
is Range -> other in this
}

override val key: Circumstance.Key<*> get() = Key
@Serializable
override val key: Circumstance.Key<*> = Key

override fun contains(other: TimeRange) = other is Range && other.start in this && other.endInclusive in this

Expand All @@ -22,9 +29,11 @@ sealed interface TimeRange : OverlappingCircumstance {

operator fun contains(other: TimeRange): Boolean

@Serializable
companion object Key : Circumstance.Key<TimeRange>
}

@Serializable
enum class Month {
January,
February,
Expand All @@ -41,6 +50,7 @@ enum class Month {
}

data class MonthDate(val day: Int, val month: Month)
@Serializable
data class Date(val day: Int, val month: Month, val year: Int) : Comparable<Date> {
override fun compareTo(other: Date): Int = when {
year.compareTo(other.year) != 0 -> year.compareTo(other.year)
Expand Down
11 changes: 10 additions & 1 deletion lib/src/main/kotlin/eu/dcotta/confis/model/sentence.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import eu.dcotta.confis.eval.QueryResponse
import eu.dcotta.confis.model.AllowanceResult.Depends
import eu.dcotta.confis.model.Obj.Anything
import eu.dcotta.confis.model.circumstance.WorldState
import kotlinx.serialization.Serializable

@Serializable
enum class Allowance {
Allow, Forbid;

Expand All @@ -22,41 +24,48 @@ enum class AllowanceResult : QueryResponse {
*/
fun computeAmbiguous(l: AllowanceResult, r: AllowanceResult) = if (l == r) l else Depends

@Serializable
sealed interface Obj {
fun render(): String

@Serializable
class Named(val name: String, val description: String? = null) : Obj {
override fun toString() = render()
override fun equals(other: Any?) = other === this || other is Named && name == other.name
override fun hashCode(): Int = name.hashCode() * 7
override fun render() = name
}

@Serializable
object Anything : Obj {
override fun render() = ""
}
}

fun Obj(named: String, description: String? = null) = Obj.Named(named, description)

interface Subject {
@Serializable
sealed interface Subject {
fun render(): String
}

@Serializable
class Action(val name: String, val description: String? = null) {
override fun toString() = render()
override fun equals(other: Any?) = other === this || other is Action && name == other.name
override fun hashCode(): Int = name.hashCode() * 71
fun render() = name
}

@Serializable
class Party(val name: String, val description: String? = null) : Subject, Obj {
override fun toString() = render()
override fun equals(other: Any?) = other === this || other is Party && name == other.name
override fun hashCode(): Int = name.hashCode() * 71
override fun render() = name
}

@Serializable
data class Sentence(val subject: Subject, val action: Action, val obj: Obj) {
/**
* whether [this] is a more general version of [other]
Expand Down
22 changes: 22 additions & 0 deletions lib/src/test/kotlin/eu/dcotta/confis/dsl/LicenseTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package eu.dcotta.confis.dsl
import eu.dcotta.confis.Sentence
import eu.dcotta.confis.asOrFail
import eu.dcotta.confis.model.Action
import eu.dcotta.confis.model.Agreement
import eu.dcotta.confis.model.Allowance
import eu.dcotta.confis.model.Allowance.Allow
import eu.dcotta.confis.model.Allowance.Forbid
Expand All @@ -12,6 +13,7 @@ import eu.dcotta.confis.model.Clause.PermissionWithCircumstances
import eu.dcotta.confis.model.Obj
import eu.dcotta.confis.model.Party
import eu.dcotta.confis.model.circumstance.Consent
import eu.dcotta.confis.model.circumstance.Month.June
import eu.dcotta.confis.model.circumstance.Purpose.Commercial
import eu.dcotta.confis.model.circumstance.Purpose.Research
import eu.dcotta.confis.model.circumstance.PurposePolicy
Expand All @@ -21,6 +23,7 @@ import io.kotest.matchers.collections.shouldContainAll
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import kotlinx.serialization.json.Json

inline fun <reified M> Any.narrowedTo() = if (this is M) this else fail("$this should be of type ${M::class}")

Expand Down Expand Up @@ -213,4 +216,23 @@ class LicenseTest : StringSpec({
)
)
}

"can jsonify simple agreement".config(enabled = false) {
val a = AgreementBuilder {
val alice by party
val use by action
val data by thing

alice may use(data) asLongAs {
within { (1 of June year 2022)..(30 of June year 2022) }
}
}
val json = Json {
allowStructuredMapKeys = true
// serializersModule = SerializersModule {
// polymorphic(Circumstance.Key::class, TimeRange.Key::class, TimeRange.Key.serializer())
// }
}
println(json.encodeToString(Agreement.serializer(), a))
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,24 @@ package eu.dcotta.confis.model
import eu.dcotta.confis.dsl.of
import eu.dcotta.confis.dsl.year
import eu.dcotta.confis.model.circumstance.Circumstance
import eu.dcotta.confis.model.circumstance.Circumstance.Key
import eu.dcotta.confis.model.circumstance.Month.May
import eu.dcotta.confis.model.circumstance.TestCircumstance
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import eu.dcotta.confis.model.circumstance.TestCircumstance.Key as TestKey

data class IntCircumstance(val i: Int) : Circumstance {
data class IntCircumstance(val i: Int) : TestCircumstance {
override val key = Companion
override fun generalises(other: Circumstance) = other is IntCircumstance && i >= other.i

companion object : Key<IntCircumstance>
companion object : TestKey<IntCircumstance>
}

data class StrCircumstance(val i: String) : Circumstance {
data class StrCircumstance(val i: String) : TestCircumstance {
override val key = Companion
override fun generalises(other: Circumstance) = other is StrCircumstance && i in other.i

companion object : Key<StrCircumstance>
companion object : TestKey<StrCircumstance>
}

class CircumstanceMapTest : StringSpec({
Expand Down
Loading

0 comments on commit 1145147

Please sign in to comment.