diff --git a/.github/codecov.yml b/.github/codecov.yml
deleted file mode 100644
index 2f490215..00000000
--- a/.github/codecov.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-codecov:
- require_ci_to_pass: yes
- notify:
- after_n_builds: 2
-
-coverage:
- precision: 2
- round: down
- range: "70...100"
-
-parsers:
- gcov:
- branch_detection:
- conditional: yes
- loop: yes
- method: no
- macro: no
-
-comment:
- layout: "reach,diff,flags,tree"
- behavior: default
- require_changes: no
\ No newline at end of file
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index 210a9834..38402b0f 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -10,11 +10,12 @@ jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - name: Set up JDK 17
- uses: actions/setup-java@v1
+ - uses: actions/checkout@v4
+ - name: Set up JDK 21
+ uses: actions/setup-java@v3
with:
- java-version: 17
+ distribution: 'temurin'
+ java-version: 21
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build, Test, Generate Scoverage Report with Gradle
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index 90774dc8..9ebd42c8 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -12,16 +12,17 @@ jobs:
TESTCONTAINERS_RYUK_DISABLED: true
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - name: Set up JDK 17
- uses: actions/setup-java@v1
+ - uses: actions/checkout@v4
+ - name: Set up JDK 21
+ uses: actions/setup-java@v3
with:
- java-version: 17
+ distribution: 'temurin'
+ java-version: 21
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build, Test, Generate Scoverage Report with Gradle
- run: ./gradlew build test
-# - name: Upload coverage to Codecov
-# uses: codecov/codecov-action@v1
-# with:
-# directory: ./build/reports/scoverage
+ run: ./gradlew clean check
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v4-beta
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.github/workflows/publish-snapshots.yml b/.github/workflows/publish-snapshots.yml
index 354998b1..9e179107 100644
--- a/.github/workflows/publish-snapshots.yml
+++ b/.github/workflows/publish-snapshots.yml
@@ -8,11 +8,12 @@ jobs:
push-snapshots:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - name: Set up JDK 17
- uses: actions/setup-java@v1
+ - uses: actions/checkout@v4
+ - name: Set up JDK 21
+ uses: actions/setup-java@v3
with:
- java-version: 17
+ distribution: 'temurin'
+ java-version: 21
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Push latest Rudiments snapshots to Maven Snapshots repo
diff --git a/README.md b/README.md
index 1c650123..25b7c714 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# Hard Core

-[](https://codecov.io/gh/rudiments-dev/hardcore)
+[](https://codecov.io/gh/rudiments-dev/hardcore)
[](https://search.maven.org/search?q=g:%22dev.rudiments%22%20AND%20a:%22implementation%22)
Research project and bootstrap library.
diff --git a/build.gradle b/build.gradle
index 59e6d86d..aea7dc89 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,61 +1,20 @@
-buildscript {
- repositories {
- maven {
- url "https://plugins.gradle.org/m2/"
- }
- }
-}
-
-def base_version = '0.4-SNAPSHOT'
-
-allprojects {
- group 'dev.rudiments'
- version base_version
-
- repositories {
- mavenLocal()
- mavenCentral()
- }
+plugins {
+ id 'dev.rudiments.scala-app-conventions'
+ id 'jacoco-report-aggregation'
}
-def scalaModules() {
- subprojects.findAll { new File(it.projectDir, 'src/main/scala').directory }
+dependencies {
+ jacocoAggregation project(':example')
}
-configure(scalaModules()) {
- apply plugin: 'java'
- apply plugin: 'scala'
-
- sourceCompatibility = 17
- targetCompatibility = 17
-
- test {
- reports {
- html.required = true
- junitXml.required = true
- }
- maxHeapSize = "2048m"
- testLogging {
- events "skipped", "failed"
- exceptionFormat "full"
+reporting {
+ reports {
+ testCodeCoverageReport(JacocoCoverageReport) {
+ testType = TestSuiteType.UNIT_TEST
}
}
+}
- dependencies {
- implementation 'org.scala-lang:scala-library:2.13.7'
- implementation 'com.typesafe.akka:akka-actor_2.13:2.6.17'
- implementation 'com.typesafe.akka:akka-slf4j_2.13:2.6.17'
- implementation 'com.typesafe.akka:akka-stream_2.13:2.6.17'
-
- implementation 'com.beachape:enumeratum_2.13:1.7.0'
-
- implementation 'com.typesafe.scala-logging:scala-logging_2.13:3.9.4'
- implementation 'org.slf4j:slf4j-api:1.7.32'
-
- testImplementation 'org.scalatest:scalatest_2.13:3.2.10'
- testImplementation 'org.scalatestplus:junit-4-13_2.13:3.2.10.0'
-
- testImplementation 'junit:junit:4.13.2'
- testImplementation 'ch.qos.logback:logback-classic:1.2.7'
- }
+tasks.named('check') {
+ dependsOn tasks.named('testCodeCoverageReport', JacocoReport)
}
\ No newline at end of file
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
new file mode 100644
index 00000000..4fa76d06
--- /dev/null
+++ b/buildSrc/build.gradle
@@ -0,0 +1,7 @@
+plugins {
+ id 'groovy-gradle-plugin'
+}
+
+repositories {
+ gradlePluginPortal()
+}
diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle
new file mode 100644
index 00000000..bde9e603
--- /dev/null
+++ b/buildSrc/settings.gradle
@@ -0,0 +1,7 @@
+dependencyResolutionManagement {
+ versionCatalogs {
+ create('libs', { from(files("../gradle/libs.versions.toml")) })
+ }
+}
+
+rootProject.name = 'buildSrc'
diff --git a/buildSrc/src/main/groovy/dev.rudiments.scala-app-conventions.gradle b/buildSrc/src/main/groovy/dev.rudiments.scala-app-conventions.gradle
new file mode 100644
index 00000000..0f64741d
--- /dev/null
+++ b/buildSrc/src/main/groovy/dev.rudiments.scala-app-conventions.gradle
@@ -0,0 +1,4 @@
+plugins {
+ id 'dev.rudiments.scala-conventions'
+ id 'application'
+}
diff --git a/buildSrc/src/main/groovy/dev.rudiments.scala-conventions.gradle b/buildSrc/src/main/groovy/dev.rudiments.scala-conventions.gradle
new file mode 100644
index 00000000..19f7c4f3
--- /dev/null
+++ b/buildSrc/src/main/groovy/dev.rudiments.scala-conventions.gradle
@@ -0,0 +1,55 @@
+plugins {
+ id 'java'
+ id 'scala'
+ id 'jacoco'
+}
+
+def baseVersion = '0.6-SNAPSHOT'
+
+repositories {
+ mavenLocal()
+ mavenCentral()
+}
+
+group = 'dev.rudiments'
+version = baseVersion
+
+dependencies {
+ implementation 'org.scala-lang:scala3-library_3:3.3.1'
+ implementation 'org.slf4j:slf4j-api:2.0.11'
+
+
+ testImplementation 'org.scalatest:scalatest_3:3.2.17'
+ testImplementation 'org.scalatestplus:junit-5-10_3:3.2.17.0'
+
+ testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1'
+ testImplementation 'org.junit.platform:junit-platform-launcher:1.10.0'
+ testRuntimeOnly 'org.junit.platform:junit-platform-engine:1.10.0'
+
+ testRuntimeOnly 'ch.qos.logback:logback-classic:1.4.14'
+}
+
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(21)
+ }
+}
+
+test {
+ useJUnitPlatform {
+ includeEngines 'scalatest'
+ testLogging {
+ events("passed", "skipped", "failed", "standard_error")
+ }
+ }
+}
+
+jacocoTestReport {
+ dependsOn test
+ reports {
+ xml.required = true
+ html.required = false
+ }
+}
+
+check.dependsOn jacocoTestReport
diff --git a/buildSrc/src/main/groovy/dev.rudiments.scala-lib-conventions.gradle b/buildSrc/src/main/groovy/dev.rudiments.scala-lib-conventions.gradle
new file mode 100644
index 00000000..acbdacf2
--- /dev/null
+++ b/buildSrc/src/main/groovy/dev.rudiments.scala-lib-conventions.gradle
@@ -0,0 +1,4 @@
+plugins {
+ id 'dev.rudiments.scala-conventions'
+ id 'java-library'
+}
diff --git a/core/build.gradle b/core/build.gradle
index 0ce6a166..6c83c2ce 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -1,3 +1,10 @@
+plugins {
+ id 'dev.rudiments.scala-lib-conventions'
+}
+
dependencies {
+ implementation 'io.github.java-diff-utils:java-diff-utils:4.12'
+ implementation 'io.circe:circe-core_3:0.15.0-M1'
+ implementation 'io.circe:circe-generic_3:0.15.0-M1'
}
\ No newline at end of file
diff --git a/core/src/main/scala/dev/rudiments/codecs/Codec.scala b/core/src/main/scala/dev/rudiments/codecs/Codec.scala
new file mode 100644
index 00000000..cb841a5e
--- /dev/null
+++ b/core/src/main/scala/dev/rudiments/codecs/Codec.scala
@@ -0,0 +1,42 @@
+package dev.rudiments.codecs
+
+class Encoder[A, B](val en: A => Result[B]) extends OneWay(en) {
+ def apply(a: A): Result[B] = this.en(a)
+ def toCodec(de: B => Result[A]): Codec[A, B] = Codec(en, de)
+}
+object Encoder {
+ def apply[A, B](f: A => B): Encoder[A, B] = new Encoder(f.andThen(r => Result.Ok(r)))
+ //TODO if error in B
+}
+
+class Decoder[A, B](val de: B => Result[A]) extends Encoder[B, A](de) {}
+object Decoder {
+ def apply[A, B](f: B => A): Decoder[A, B] = new Decoder(f.andThen(r => Result.Ok(r)))
+}
+
+class Codec[A, B](en: A => Result[B], de: B => Result[A]) {
+ def bimap[C](
+ f: B => C, g: C => B
+ ): Codec[A, C] = Codec(
+ en.andThen(_.map(f)), g.andThen(de)
+ )
+}
+
+enum Result[A] {
+ case Error(e: Exception)
+ case Ok(value: A)
+
+ def map[B](f: A => B): Result[B] = this match {
+ case Result.Error(e) => Result.Error(e)
+ case Result.Ok(v) => Result.Ok(f(v))
+ }
+
+ def flatMap[B](f: A => Result[B]): Result[B] = this match {
+ case Result.Error(e) => Result.Error(e)
+ case Result.Ok(v) => f(v)
+ }
+}
+
+class OneWay[A, B](val t: A => Result[B]) {
+ def map[C](f: B => C): OneWay[A, C] = OneWay(t.andThen(_.map(f)))
+}
diff --git a/core/src/main/scala/dev/rudiments/codecs/MJ.scala b/core/src/main/scala/dev/rudiments/codecs/MJ.scala
new file mode 100644
index 00000000..f86b5257
--- /dev/null
+++ b/core/src/main/scala/dev/rudiments/codecs/MJ.scala
@@ -0,0 +1,63 @@
+package dev.rudiments.codecs
+
+import dev.rudiments.utils.Log
+
+import scala.compiletime.{constValue, erasedValue, error, summonFrom}
+import scala.deriving.Mirror
+
+object MJ extends Log {
+ type En[A] = OneWay[A, TS]
+
+ given intToNumber: OneWay[Int, TS.Number] = OneWay(i => Result.Ok(TS.Number(i)))
+ given strToText: OneWay[String, TS.Text] = OneWay(s => Result.Ok(TS.Text(s)))
+ given many[S, T <: TS](using t: OneWay[S, T]): OneWay[Iterable[S], TS.Many] = OneWay(l =>
+ l.foldLeft(Result.Ok[TS.Many](TS.Many(Seq.empty))) { (acc, i) =>
+ for {
+ el <- t.t(i)
+ many <- acc
+ } yield TS.Many(many.of :+ el)
+ }
+ )
+ given index[K, V, T <: TS](using keys: OneWay[K, TS.Text], values: OneWay[V, T]): OneWay[Map[K, V], TS.Idx] = OneWay( m =>
+ m.foldLeft(Result.Ok[TS.Idx](TS.Idx(Map.empty))) { case (acc, (k, v)) =>
+ for {
+ key <- keys.t(k)
+ value <- values.t(v)
+ many <- acc
+ } yield TS.Idx(many.of + (key -> value))
+ }
+ )
+
+ inline final def summonLabelsRec[T <: Tuple]: List[String] = inline erasedValue[T] match {
+ case _: EmptyTuple => Nil
+ case _: (t *: ts) => constValue[t].asInstanceOf[String] :: summonLabelsRec[ts]
+ }
+
+ inline final def summonEncoder[A]: En[A] = summonFrom {
+ case encodeA: En[A] => encodeA
+ case _: Mirror.Of[A] => derived[A]
+ }
+
+ inline final def summonEncodersRec[A <: Tuple]: List[En[_]] =
+ inline erasedValue[A] match {
+ case _: EmptyTuple => Nil
+ case _: (t *: ts) => summonEncoder[t] :: summonEncodersRec[ts]
+ }
+
+ inline final def derived[A](using A: Mirror.Of[A]): En[A] = {
+ val name = constValue[A.MirroredLabel].asInstanceOf[String]
+ val labels = summonLabelsRec[A.MirroredElemLabels].toArray
+ val encoders = summonEncodersRec[A.MirroredElemTypes].toArray
+
+
+ log.info("{} with labels {}", name, labels.mkString("[", ", ", "]"))
+ ???
+ }
+}
+
+enum TS {
+ case Number(i: Int)
+ case Text(s: String)
+ case Many(of: Seq[TS])
+ case Idx(of: Map[TS.Text, TS])
+}
\ No newline at end of file
diff --git a/core/src/main/scala/dev/rudiments/codecs/MirrorInfo.scala b/core/src/main/scala/dev/rudiments/codecs/MirrorInfo.scala
new file mode 100644
index 00000000..690a7e54
--- /dev/null
+++ b/core/src/main/scala/dev/rudiments/codecs/MirrorInfo.scala
@@ -0,0 +1,38 @@
+package dev.rudiments.codecs
+
+import scala.compiletime.{constValue, erasedValue, error, summonFrom}
+import scala.deriving.Mirror
+
+case class MirrorInfo[A](
+ name: String,
+ fields: Seq[(String, MirrorInfo[_])]
+)
+
+object MirrorInfo {
+ given intInfo: MirrorInfo[Int] = MirrorInfo("Int", Seq.empty)
+ given strInfo: MirrorInfo[String] = MirrorInfo("String", Seq.empty)
+ given seqInfo[T]: MirrorInfo[Seq[T]] = MirrorInfo("Seq of", Seq.empty)
+
+ inline final def summonInfo[A]: MirrorInfo[A] = summonFrom {
+ case i: MirrorInfo[A] => i
+ case _: Mirror.Of[A] => apply[A]
+ }
+
+ inline final def fieldsInfo[A <: Tuple]: List[MirrorInfo[_]] = inline erasedValue[A] match {
+ case _: EmptyTuple => Nil
+ case _: (t *: ts) => summonInfo[t] :: fieldsInfo[ts]
+ }
+
+ inline final def summonLabelsRec[T <: Tuple]: List[String] = inline erasedValue[T] match {
+ case _: EmptyTuple => Nil
+ case _: (t *: ts) => constValue[t].asInstanceOf[String] :: summonLabelsRec[ts]
+ }
+
+ inline final def apply[A](using A: Mirror.Of[A]): MirrorInfo[A] = {
+ val name = constValue[A.MirroredLabel].asInstanceOf[String]
+ val labels = summonLabelsRec[A.MirroredElemLabels]
+ val fields = fieldsInfo[A.MirroredElemTypes]
+
+ MirrorInfo(name, labels.zip(fields))
+ }
+}
diff --git a/core/src/main/scala/dev/rudiments/hardcore/AgentCrud.scala b/core/src/main/scala/dev/rudiments/hardcore/AgentCrud.scala
deleted file mode 100644
index ec845bb4..00000000
--- a/core/src/main/scala/dev/rudiments/hardcore/AgentCrud.scala
+++ /dev/null
@@ -1,53 +0,0 @@
-package dev.rudiments.hardcore
-
-import dev.rudiments.hardcore.CRUD.{I, O}
-
-trait AgentCrud extends Agent {
-
- def ask(where: Location, what: I): O = (read(where), what) match {
- case (NotExist, Create(data)) => Created(data)
- case (NotFound(_), Create(data)) => Created(data)
- case (NotExist, _) => NotExist
- case (n: NotFound, _) => n
- case (r: Readen, Read) => r
- case (Readen(found), Create(_)) => AlreadyExist(found)
- case (Readen(found), Update(data)) => Updated(found, data)
- case (Readen(found), Delete) => Deleted(found)
- case (other, another) =>
- Conflict(other, another)
- }
-
- def remember(where: Location, what: O): O
-
- def + (pair: (Location, Thing)): O = this.ask(pair._1, Create(pair._2))
- def * (pair: (Location, Thing)): O = this.ask(pair._1, Update(pair._2))
- def - (where: Location): O = this.ask(where, Delete)
-
- def += (pair: (Location, Thing)): O = this + pair match {
- case c: Created => this.remember(pair._1, c)
- case other => other
- }
-
- def *= (pair: (Location, Thing)): O = this * pair match {
- case u: Updated => this.remember(pair._1, u)
- case other => other
- }
- def -= (where: Location): O = this - where match {
- case d: Deleted => this.remember(where, d)
- case other => other
- }
-
- def := (pair: (Location, Thing)): O = (read(pair._1), pair._2) match {
- case (NotExist, Nothing) => Conflict(NotExist, Delete)
- case (NotExist, t) => this.remember(pair._1, Created(t))
- case (NotFound(_), t) => this.remember(pair._1, Created(t))
- case (Readen(r), t) if r != t => this.remember(pair._1, Updated(r, t))
- case (r@Readen(r1), t) if r1 == t => r
- case (Readen(r), Nothing) => this.remember(pair._1, Deleted(r))
- }
-
- def /! (where: Location): Node = read(where) match {
- case Readen(mem: Node) => mem
- case _ => throw new IllegalArgumentException("Not a memory")
- }
-}
diff --git a/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala b/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala
deleted file mode 100644
index 77fdc8c1..00000000
--- a/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala
+++ /dev/null
@@ -1,57 +0,0 @@
-package dev.rudiments.hardcore
-
-
-sealed trait CRUD {}
-object CRUD {
- type Evt = Event with CRUD
- type Cmd = Command with CRUD
- type O = Out with CRUD
- type I = In with CRUD
-}
-
-final case class Create(what: Thing) extends Command with CRUD
-case object Read extends Query with CRUD
-final case class Update(what: Thing) extends Command with CRUD
-case object Delete extends Command with CRUD
-
-final case class Find(p: Predicate) extends Query with CRUD
-final case class LookFor(p: Predicate) extends Query with CRUD
-final case class Dump(p: Predicate) extends Query with CRUD
-
-case object Prepare extends Query with CRUD
-case object Verify extends Query with CRUD
-final case class Commit(
- crud: Map[Location, CRUD.Evt],
- extra: Seq[(Command, Event)] = Seq.empty // for future use
-) extends Command with CRUD {
- def crudNode(): Node = Node.fromMap(crud)
- def stateNode(): Node = Node.fromEventMap(crud)
-}
-
-final case class Partner(of: Location) extends Command with CRUD
-final case class Quit(from: Location) extends Command with CRUD
-
-
-final case class Created(data: Thing) extends Event with CRUD
-final case class Readen(data: Thing) extends Report with CRUD
-final case class Updated(old: Thing, data: Thing) extends Event with CRUD
-final case class Deleted(old: Thing) extends Event with CRUD
-
-final case class Found(query: Query, values: Map[Location, Thing]) extends Report with CRUD
-case object NotExist extends Report with CRUD
-case class NotFound(missing: Location) extends Report with CRUD
-
-final case class Prepared(commit: Commit) extends Report with CRUD
-case object Identical extends Report with CRUD
-case object Valid extends Report with CRUD
-final case class Committed(commit: Commit) extends Event with CRUD
-
-final case class Partners(w: Location) extends Event with CRUD
-final case class Quited(from: Location) extends Event with CRUD
-
-
-final case class AlreadyExist(data: Thing) extends Error with CRUD
-final case class Conflict(that: Message, other: Message) extends Error with CRUD
-final case class MultiError(errors: Map[Location, Out]) extends Error with CRUD
-case object NotImplemented extends Error with CRUD
-case object NotSupported extends Error with CRUD
diff --git a/core/src/main/scala/dev/rudiments/hardcore/Graph.scala b/core/src/main/scala/dev/rudiments/hardcore/Graph.scala
new file mode 100644
index 00000000..7ccf5c9e
--- /dev/null
+++ b/core/src/main/scala/dev/rudiments/hardcore/Graph.scala
@@ -0,0 +1,106 @@
+package dev.rudiments.hardcore
+
+import dev.rudiments.codecs.{ Encoder, Result }
+import dev.rudiments.hardcore.Graph.{ AroundNode, Edge, Edges, JointGraph }
+
+case class Graph[K, +N, +E](
+ nodes: Map[K, N],
+ edges: Edges[K, E]
+) {
+ lazy val edgesFrom: Map[K, Edges[K, E]] = edges.groupBy(_.from)
+ lazy val edgesTo: Map[K, Edges[K, E]] = edges.groupBy(_.to)
+
+ def roots(f: Edge[K, E] => Boolean = _ => true): Set[K] =
+ edgesTo.filter { case (k, v) => !v.exists(f) }.keySet
+
+ def map[A, B](f: N => A, g: E => B): Graph[K, A, B] = Graph(
+ nodes.map((k, v) => k -> f(v)),
+ edges.map(e => e.copy(value = g(e.value)))
+ )
+ def collect[A, B](f: PartialFunction[AroundNode[K, N, E], AroundNode[K, A, B]]): Graph[K, A, B] = {
+ val newNodes = nodes
+ .map { case (k, n) => AroundNode(
+ key = k,
+ node = n,
+ from = edgesFrom.getOrElse(k, Seq.empty),
+ to = edgesTo.getOrElse(k, Seq.empty)
+ )}
+ .flatMap { a => if (f.isDefinedAt(a)) Some(f.apply(a)) else None }
+ .toSeq
+
+ val from = newNodes.flatMap(a => a.from)
+ val to = newNodes.flatMap(a => a.to)
+ val extra = to.toSet -- from.toSet
+
+ Graph(
+ newNodes.map(a => a.key -> a.node).toMap,
+ from ++ to.filter(extra.contains)
+ )
+ }
+
+ def filter(f: (K, N) => Boolean): Graph[K, N, E] = {
+ val n = nodes.filter{ case (k, v) => f(k, v) }
+ val keys = n.keySet
+ Graph(
+ nodes = n,
+ edges = this.edges.filter(e => keys.contains(e.to) && keys.contains(e.from))
+ )
+ }
+
+ def isSealed: Boolean = disjointed.isEmpty
+
+ def disjointed: Edges[K, E] = {
+ val fromKeys = edgesFrom.keySet
+ val toKeys = edgesTo.keySet
+ edges.filterNot(e => fromKeys.contains(e.from)) ++ edges.filterNot(e => toKeys.contains(e.to))
+ }
+
+ def join[N1 >: N, E1 >: E](that: Graph[K, N1, E1], joints: Edges[K, E1]): Graph[K, N1, E1] = Graph(
+ nodes = this.nodes ++ that.nodes,
+ edges = this.edges ++ that.edges ++ joints
+ )
+ def split(keys: Set[K]): JointGraph[K, N, E] = {
+ val cutNodeKeys = this.nodes.keySet -- keys
+ val joints = this.edges.filter { case Edge(from, to, _) =>
+ cutNodeKeys.contains(from) && keys.contains(to) || keys.contains(from) && cutNodeKeys.contains(to)
+ }
+ JointGraph(
+ this.filter { (k, _) => keys.contains(k) },
+ joints,
+ this.filter { (k, _) => cutNodeKeys.contains(k) }
+ )
+ }
+
+ def to[A, N1 >: N, E1 >: E](using en: Encoder[Graph[K, N1, E1], A]): Result[A] = en.en(this)
+}
+
+object Graph {
+ type Edges[A, +B] = Seq[Edge[A, B]]
+
+ case class Edge[K, +E](from: K, to: K, value: E)
+ case class Item[K, +N, +E](key: K, node: N, edges: Seq[(Int, E)]) // is a monadic cus it is an element in array!
+ case class AroundNode[K, +N, +E](key: K, node: N, from: Seq[Edge[K, E]], to: Seq[Edge[K, E]])
+
+ def empty[K, N, E]: Graph[K, N, E] = Graph(Map.empty[K, N], Seq.empty[Edge[K, E]])
+
+ case class SeqGraph[K, N, E](
+ items: Seq[Item[K, N, E]],
+ keys: Map[K, Int],
+ ) {
+ def to[A](using en: Encoder[SeqGraph[K, N, E], A]): Result[A] = en.en(this)
+ }
+
+ def toSeqGraph[K, N, E](g: Graph[K, N, E]): SeqGraph[K, N, E] = {
+ val indexed = g.nodes.toSeq.zipWithIndex
+ val keys = indexed.map { case ((k, n), i) => k -> i }.toMap
+ SeqGraph(
+ indexed.map { case ((k, n), i) =>
+ Item[K, N, E](k, n, g.edgesFrom.getOrElse(k, Seq.empty).map(e => keys(e.to) -> e.value))
+ }, keys
+ )
+ }
+
+ case class JointGraph[K, +N, +E](g: Graph[K, N, E], joints: Edges[K, E], h: Graph[K, N, E]) {
+ //def to[A](using en: Encoder[JointGraph[K, N, E], A]): Result[A] = en.en(this)
+ }
+}
diff --git a/core/src/main/scala/dev/rudiments/hardcore/Initial.scala b/core/src/main/scala/dev/rudiments/hardcore/Initial.scala
deleted file mode 100644
index eac18949..00000000
--- a/core/src/main/scala/dev/rudiments/hardcore/Initial.scala
+++ /dev/null
@@ -1,150 +0,0 @@
-package dev.rudiments.hardcore
-
-object Initial {
- val types: ID = ID("types")
- private val predicate: Declared = Declared(types / "Predicate")
-
- def init(ctx: Memory): Unit = {
- val tx = new Tx(ctx)
- tx += types -> Node.empty
-
- {
- tx += types / "ID" -> Type(Field("key", Anything))
- tx += types / "Path" -> Type(Field("ids", Enlist(tx ! types / "ID")))
- tx += types / "Root" -> Nothing
- tx += types / "Unmatched" -> Nothing
-
- tx += types / "Location" -> Node.partnership(types, Seq("ID", "Path", "Root", "Unmatched"))
- }
-
- {
- tx += types / "Number" -> Type(
- Field("from", Anything),
- Field("to", Anything)
- )
- tx += types / "Text" -> Type(
- Field("maxSize", Number(0, Int.MaxValue))
- )
- tx += types / "Bool" -> Nothing
- tx += types / "Binary" -> Nothing
-
- tx += types / "Date" -> Nothing
- tx += types / "Time" -> Nothing
- tx += types / "Timestamp" -> Nothing
- tx += types / "Temporal" -> Node.partnership(types, Seq("Date", "Time", "Timestamp"))
-
- tx += types / "Plain" -> Node.partnership(types, Seq("Text", "Number", "Bool", "Binary", "Temporal"))
- }
-
- {
- tx += types / "Anything" -> Nothing
- tx += types / "Nothing" -> Nothing
-
- tx += types / "Field" -> Type(
- Field("name", Text(Int.MaxValue)),
- Field("of", predicate)
- )
- tx += types / "Type" -> Type(
- Field("fields", Enlist(tx ! types / "Field"))
- )
- tx += types / "Enlist" -> Type(Field("of", predicate))
- tx += types / "Index" -> Type(Field("of", predicate), Field("over", predicate))
- tx += types / "AnyOf" -> Type(Field("p", Enlist(predicate)))
- tx += types / "Link" -> Type(
- Field("where", tx ! (types / "Location")),
- Field("what", predicate)
- )
- tx += types / "Declared" -> Type(Field("where", tx ! (types / "Location")))
-
- tx += types / "Predicate" -> Node.partnership(types, Seq(
- "Anything", "Nothing", "Plain",
- "Type", "Enlist", "Index", "AnyOf",
- "Link", "Declared"
- ))
- }
-
- {
- tx += types / "Data" -> Type(
- Field("what", predicate),
- Field("data", Anything)
- )
-
- tx += types / "Agent" -> Node.partnership(types, Seq("Node"))
- tx += types / "Node" -> Type(
- Field("self", Anything),
- Field("keyIs", predicate),
- Field("leafIs", predicate)
- )
- }
-
- {
- tx += types / "Message" -> Node.partnership(types, Seq("In", "Out"))
- tx += types / "In" -> Node.partnership(types, Seq("Query", "Command"))
- tx += types / "Out" -> Node.partnership(types, Seq("Report", "Event", "Error"))
- tx += types / "Query" -> Node.partnership(types, Seq("Read", "Find", "LookFor", "Dump", "Prepare", "Verify"))
- tx += types / "Command" -> Node.partnership(types, Seq("Create", "Update", "Delete", "Commit"))
- tx += types / "Report" -> Node.partnership(types, Seq("Readen", "Found", "NotExist", "NotFound", "Prepared", "Valid"))
- tx += types / "Event" -> Node.partnership(types, Seq("Created", "Updated", "Deleted", "Committed"))
- tx += types / "Error" -> Node.partnership(types, Seq("AlreadyExist", "Conflict", "NotImplemented"))
-
- tx += types / "CRUD" -> Node.partnership(types, Seq(
- "Create", "Read", "Update", "Delete", "Find", "Prepare", "Verify", "Commit",
- "Created", "Readen", "Updated", "Deleted", "Found", "Prepared", "Valid", "Committed",
- "NotExist", "NotFound", "AlreadyExist", "Conflict", "NotImplemented"
- ))
- }
-
- {
- tx += types / "Create" -> Type(Field("what", Anything))
- tx += types / "Read" -> Nothing
- tx += types / "Update" -> Type(Field("what", Anything))
- tx += types / "Delete" -> Nothing
- tx += types / "Find" -> Type(Field("p", predicate))
- tx += types / "LookFor" -> Type(Field("p", predicate))
- tx += types / "Dump" -> Type(Field("p", predicate))
- tx += types / "Prepare" -> Nothing
- tx += types / "Verify" -> Nothing
- tx += types / "Commit" -> Type(
- Field("crud", Index(tx ! types / "Location", Declared(types / "Event")))
- )
-
- tx += types / "Created" -> Type(Field("data", Anything))
- tx += types / "Readen" -> Type(Field("data", Anything))
- tx += types / "Updated" -> Type(Field("old", Anything), Field("data", Anything))
- tx += types / "Deleted" -> Type(Field("old", Anything))
- tx += types / "Found" -> Type(
- Field("query", tx ! types / ID("Query")),
- Field("values", Index(tx ! types / "Location", Anything))
- )
- tx += types / "NotExist" -> Nothing
- tx += types / "NotFound" -> Type(Field("missing", tx ! types / "Location"))
- tx += types / "Prepared" -> Type(Field("commit", Declared(types / "Commit")))
- tx += types / "Identical" -> Nothing
- tx += types / "Valid" -> Nothing
- tx += types / "Committed" -> Type(Field("commit", Declared(types / "Commit")))
-
- tx += types / "AlreadyExist" -> Type(Field("data", Anything))
- tx += types / "Conflict" -> Type(
- Field("that", Declared(types / "Message")),
- Field("other", Declared(types / "Message"))
- )
- tx += types / "MultiError" -> Type(
- Field("errors", Index(tx ! types / "Location", Declared(types / "Out")))
- )
- tx += types / "NotImplemented" -> Nothing
- tx += types / "NotSupported" -> Nothing
- }
-
- val prepared = tx.>>
- prepared match {
- case Prepared(c) => ctx << c match {
- case Committed(cmt) =>
- cmt
- case other =>
- throw new IllegalStateException("Initial commit failed")
- }
- case other =>
- throw new IllegalStateException("Initial commit not prepared")
- }
- }
-}
diff --git a/core/src/main/scala/dev/rudiments/hardcore/Location.scala b/core/src/main/scala/dev/rudiments/hardcore/Location.scala
new file mode 100644
index 00000000..225aac42
--- /dev/null
+++ b/core/src/main/scala/dev/rudiments/hardcore/Location.scala
@@ -0,0 +1,5 @@
+package dev.rudiments.hardcore
+
+case class Location[T](where: T) {}
+type ID = Location[String]
+type Path[T <: Tuple] = Location[T]
diff --git a/core/src/main/scala/dev/rudiments/hardcore/Memory.scala b/core/src/main/scala/dev/rudiments/hardcore/Memory.scala
deleted file mode 100644
index 70302af9..00000000
--- a/core/src/main/scala/dev/rudiments/hardcore/Memory.scala
+++ /dev/null
@@ -1,75 +0,0 @@
-package dev.rudiments.hardcore
-
-import dev.rudiments.hardcore.CRUD.{Evt, O}
-import dev.rudiments.hardcore.Memory.commits
-
-case class Memory(
- node: Node = Node.empty
-) extends AgentCrud {
- this += commits -> Node.empty
- Initial.init(this)
-
- private def nodeEvent(where: Location, what: Evt): Evt = node.remember(where, what) match {
- case evt: Evt => evt
- case other =>
- throw new IllegalArgumentException(s"whut? $other")
- }
-
- override def read(where: Location): O = node.read(where)
-
- override def remember(where: Location, via: O): O = {
- (this ? where, via) match {
- case (NotExist, c: Created) => nodeEvent(where, c)
- case (NotFound(_), c: Created) => nodeEvent(where, c)
- case (Readen(_: Node), c@Created(_: Data)) => nodeEvent(where, c)
- case (Readen(n: Node), u@Updated(old, _: Data)) if n.self == old => nodeEvent(where, u)
- case (Readen(r), Created(_)) =>
- AlreadyExist(r)
- case (Readen(r), Updated(u, data)) if r == u => nodeEvent(where, Updated(r, data))
- case (Readen(r), Deleted(d)) if r == d => nodeEvent(where, Deleted(r))
- case (NotExist, Committed(cmt)) =>
- val n = Node.empty
- node += where -> n
- commit(where, n, cmt) //assuming commit contains create for root
- case (NotFound(miss), Committed(cmt)) =>
- val n = Node.empty
- node += where -> n
- commit(where, n, cmt)
- case (Readen(n: Node), Committed(cmt)) =>
- commit(where, n, cmt)
- case (found, other) => Conflict(found, other)
- }
- }
-
- override def report(q: Query): O = node.report(q)
-
- private def commit(where: Location, n: Node, cmt: Commit): O = {
- val remember = Commit(cmt.crud.map { case (l, evt) => where / l -> evt })
- val p = commits / remember.hashCode().toString
- node += p -> remember match {
- case _: Created =>
- n.commit(cmt)
- case err: Error =>
- err
- }
- }
-
- def << (c: Commit) : O = this.remember(Root, Committed(c))
-}
-
-object Memory {
- val commits: ID = ID("commits")
-
- val reducer: PartialFunction[(Evt, Evt), O] = {
- case ( Created(c1), Created(_)) => AlreadyExist(c1)
- case ( Created(c1), u@Updated(u2, _)) if c1 == u2 => u
- case ( Created(c1), d@Deleted(d2)) if c1 == d2 => d
-
- case ( Updated(_, u1), Created(_)) => AlreadyExist(u1)
- case ( Updated(_, u1), u@Updated(u2, _)) if u1 == u2 => u
- case ( Updated(_, u1), d@Deleted(d2)) if u1 == d2 => d
-
- case ( Deleted(_), c@Created(_)) => c
- case (that, other) /* unfitting updates */ => Conflict(that, other)
- }
-}
diff --git a/core/src/main/scala/dev/rudiments/hardcore/Messages.scala b/core/src/main/scala/dev/rudiments/hardcore/Messages.scala
new file mode 100644
index 00000000..ba3a1694
--- /dev/null
+++ b/core/src/main/scala/dev/rudiments/hardcore/Messages.scala
@@ -0,0 +1,22 @@
+package dev.rudiments.hardcore
+
+sealed trait Message {}
+
+sealed trait Event extends Message {}
+
+case class Created[K, B, L](value: L | Tree[K, B, L]) extends Event
+case class Updated[K, B, L](old: L | Tree[K, B, L], value: L | Tree[K, B, L]) extends Event
+case class Deleted[K, B, L](old: L | Tree[K, B, L]) extends Event
+case class Same[K, B, L](value: L | Tree[K, B, L]) extends Event
+case class Readen[K, B, L](value: L | Tree[K, B, L]) extends Event
+case class Commit[K](events: Seq[(List[K], Event)]) extends Event
+
+sealed trait Error extends Message {
+ final inline def throwIt(): Unit = throw asException
+ final inline def asException: GotError = new GotError(this)
+}
+case class NotFound[K](path: List[K]) extends Error
+case class LeafOnTheWay[K](k: K, path: List[K]) extends Error
+case class Conflict(actual: Message, cause: Message) extends Error
+
+final class GotError(err: Error) extends RuntimeException(err.toString) {}
\ No newline at end of file
diff --git a/core/src/main/scala/dev/rudiments/hardcore/Node.scala b/core/src/main/scala/dev/rudiments/hardcore/Node.scala
deleted file mode 100644
index eacefc64..00000000
--- a/core/src/main/scala/dev/rudiments/hardcore/Node.scala
+++ /dev/null
@@ -1,365 +0,0 @@
-package dev.rudiments.hardcore
-
-import dev.rudiments.hardcore.CRUD.{Evt, I, O}
-
-import scala.collection.mutable
-
-case class Node(
- var self: Thing = Nothing,
- leafs: mutable.Map[ID, Thing] = mutable.Map.empty,
- branches: mutable.Map[ID, Node] = mutable.Map.empty,
- relations: mutable.Map[Location, Seq[Location]] = mutable.Map.empty,
- keyIs: Predicate = Text(1024),
- leafIs: Predicate = Anything
-) extends AgentCrud {
- override def read(where: Location): O = {
- where match {
- case Root => Readen(this)
-
- case id: ID =>
- (leafs.get(id), branches.get(id)) match {
- case (Some(l), Some(b)) =>
- Conflict(Readen(l), Readen(b))
- case (Some(leaf), None) => Readen(leaf)
- case (None, Some(b)) => Readen(b)
- case (None, None) => NotExist
- }
-
- case path: Path =>
- branches.get(path.head) match {
- case Some(node) => node.read(path.tail)
- case None => NotFound(path)
- }
-
- case _ =>
- NotImplemented
- }
- }
-
- def rememberSelf(what: O): O = what match {
- case Identical => Identical
- case Created(n: Node) =>
- NotSupported
- case c@Created(thing) => if(tags().contains(Node.Self)) {
- AlreadyExist(this.self)
- } else {
- this.self = thing
- c
- }
- case u@Updated(old: Node, n: Node) =>
- this.self = n.self //TODO - update checks
- u
- case Updated(old: Thing, n: Node) =>
- NotSupported
- case Updated(old: Node, n: Thing) =>
- NotSupported
- case u@Updated(old, newby) => if(tags().contains(Node.Self)) {
- if(old == this.self) {
- this.self = newby
- u
- } else {
- Conflict(Readen(this.self), u)
- }
- } else {
- NotExist
- }
- case d@Deleted(old) => if(tags().contains(Node.Self)) {
- if(old == this.self) {
- this.self = Nothing
- d
- } else {
- Conflict(Readen(this.self), d)
- }
- } else {
- NotExist
- }
- case other =>
- NotSupported
- }
-
- def search(path: Path)(effect: (Node, O) => O): O = {
- this.read(path.dropTail) match {
- case Readen(n: Node) => effect(n, n.read(path.last))
- case err: Error => err
- case nf: NotFound => nf
- case NotExist =>
- NotExist
- case other => Conflict(Readen(path.dropTail), other)
- }
- }
-
- def redirect(path: Path, what: O): O = {
- search(path) {
- case (_, Readen(l: Node)) => l.rememberSelf(what)
- case (n, Readen(thing)) => n.asContainer(path.last, what)
- case (n, NotExist) =>
- n.asContainer(path.last, what)
- }
- }
-
- def navigate(asStrings: Seq[String]): (Node, Location) = {
- if(asStrings.isEmpty) {
- (this, Root)
- } else {
- val id = decodeKey(asStrings.head) //TODO index all paths of nodes as stringSeq and search
- if(asStrings.size == 1) {
- (this, id)
- } else {
- read(id) match {
- case Readen(n: Node) =>
- n.navigate(asStrings.tail) match {
- case p@(_, Unmatched) => p
- case (n, l) => (n, id / l)
- }
- case other => (this, Unmatched)
- }
- }
- }
- }
-
- def asContainer(id: ID, what: O): O = {
- this.read(id) match {
- case NotExist =>
- what match {
- case c@Created(n: Node) =>
- this.branches += id -> n
- c
- case c@Created(thing) =>
- this.leafs += id -> thing
- c
- case other => NotExist
- }
-
- case r@Readen(n: Node) =>
- what match {
- case c@Created(_) => AlreadyExist(n)
- case u: Updated => ???
- case d@Deleted(old) if old == n =>
- this.branches -= id
- d
- case d: Deleted => Conflict(r, d)
- case other => n.rememberSelf(other)
- }
- case r@Readen(thing) =>
- what match {
- case _: Created => AlreadyExist(thing)
-
- case u@Updated(old, t) if old == thing =>
- this.leafs += id -> t
- u
- case u: Updated => Conflict(r, u)
-
- case d@Deleted(old) if old == thing =>
- this.leafs -= id
- d
- case d@Deleted(old) => Conflict(r, d)
-
- case other => NotImplemented
- }
- }
- }
-
- override def remember(where: Location, what: O): O = {
- where match {
- case Root =>
- this.rememberSelf(what)
- case id: ID =>
- this.asContainer(id, what)
- case path: Path =>
- this.redirect(path, what)
- case other =>
- throw new IllegalArgumentException(s"Not supported location: $other")
- }
- }
-
- override def report(q: Query): O = q match {
- case Read => Readen(this)
- case f@Find(Anything) => Found(f, leafs.toMap)
- case lf@LookFor(Anything) => Found(lf, structure)
- case d@Dump(Anything) => Found(d, everything())
- case Prepare => NotImplemented
- case Verify => ???
- case other => NotImplemented
- }
-
- def commit(c: Commit): O = {
- val ordered = c.crud.keys.toSeq.sorted(Location)
- val output = ordered.map { l => l -> remember(l, c.crud(l)) }.toMap
- val errors = output.collect {
- case p@(_, _: Error) => p
- case p@(_, NotExist) => p
- case p@(_, _: NotFound) => p
- }
-
- if(errors.isEmpty) {
- Committed(c)
- } else {
- MultiError(errors) //TODO rollback?
- }
- }
-
- def << (in: I) : O = in match {
- case q: Query => this.report(q)
- case c: Commit => this.commit(c)
- }
-
- def everything(prefix: Location = Root): Map[Location, Thing] = {
- val s = Map[Location, Thing](Root -> this.selfCopy())
- val l = this.leafs.toMap
- val b = branches.map { case (k, v) => k -> v.selfCopy() }.toMap
- val deep = branches.flatMap { case (id, b) => b.everything(id) }.toMap
- val all = s ++ l ++ b ++ deep
- all.map { case (k, v) =>
- prefix / k -> v
- }
- }
-
- def everything[K <: Thing, V <: Thing](prefix: Location, f: K => V): Map[Location, V] = {
- everything(prefix).map { case (k, v: K) => k -> f(v) }
- }
-
- def reconcile(to: Node): Map[Location, O] = {
- val source = this.everything()
- val target = to.everything()
- val keys = (source.keySet ++ target.keySet).toSeq.sorted(Location)
-
- keys.map { k =>
- (source.get(k), target.get(k)) match {
- case (None, None) => throw new IllegalStateException("How this happen?")
- case (None, Some(incoming)) => k -> Created(incoming)
- case (Some(existing), Some(incoming)) if existing == incoming => k -> Identical
- case (Some(existing), Some(incoming)) if existing != incoming => k -> Updated(existing, incoming)
- case (Some(existing), None) => k -> Deleted(existing)
- }
- }.toMap
- }
-
- def structure: Map[Location, Thing] = {
- val store = mutable.Map.empty[Location, Thing]
- this.branches.foreach { case (id, m) =>
- store += id -> m.copy(leafs = mutable.Map.empty, branches = mutable.Map.empty)
- store ++= m.structure.map { case (k, v) => id / k -> v }
- }
- store.toMap
- }
-
- def decodeAndReadLocation(s: Seq[String]): (Location, Thing) = s.size match {
- case 0 => (Root, this)
- case 1 =>
- val id = decodeKey(s.head)
- (id, read(id))
- case _ =>
- val id = decodeKey(s.head)
- branches.get(id) match {
- case Some(found) =>
- val rec = found.decodeAndReadLocation(s.tail)
- id / rec._1 -> rec._2 //some pattern are looking at me, like `id / (pair) => id / pair._1 -> pair._2`
- case None => Location(s) -> NotFound(Location(s))
- }
- }
-
- private def decodeKey(s: String): ID = keyIs match {
- case Text(_) => ID(s)
- case Number(Long.MinValue, Long.MaxValue) => ID(s.toLong)
- case other => throw new IllegalArgumentException(s"Not supported key decoding: $other")
- }
-
- override def toString: String = s"Node(key: $keyIs, leafs: $leafIs, total: ${leafs.size}/${branches.size})"
-
- def tags(): Set[Node.Tag] = { //TODO Agent on Tag + control of update
- Seq(
- (self != Nothing) -> Node.Self,
- branches.nonEmpty -> Node.Branches,
- leafs.nonEmpty -> Node.Leafs,
- (branches.nonEmpty || leafs.nonEmpty) -> Node.Container,
- relations.nonEmpty -> Node.Relations
- ).collect { case (true, n) => n }.toSet
- }
-
- def selfCopy(): Node = this.copy(
- leafs = mutable.Map.empty,
- branches = mutable.Map.empty
- )
-}
-
-
-object Node {
- sealed trait Tag {}
- case object Self extends Tag
- case object Container extends Tag
- case object Leafs extends Tag
- case object Branches extends Tag
- case object Relations extends Tag
-
- val partnersId: ID = ID("Partners")
-
- def empty: Node = Node(Nothing)
-
- def fromEventMap(from: Map[Location, O]): Node = {
- val initial = from.get(Root) match {
- case Some(Identical) => Node.empty
- case Some(Created(n: Node)) => n
- case Some(Created(t: Thing)) => Node(t)
- case Some(Updated(n1, n2: Node)) if n1 == Node.empty => n2
- case None => Node.empty
- case other => throw new IllegalArgumentException(s"What happened? $other")
- }
- val ordered = (from.keySet - Root).toSeq.sorted(Location)
- ordered.foldLeft(initial) { case (acc, l) =>
- from(l) match {
- case Identical => acc //DO nothing
- case evt: Evt =>
- acc.remember(l, evt) match {
- case _: Evt => acc
- case Identical => acc
- case err: Error => throw new IllegalStateException(s"Error while recreating node: $l -> $err")
- case other => throw new IllegalStateException(s"Unexpected while recreating node: $l -> $other")
- }
- case report: Report =>
- throw new IllegalArgumentException(s"Expecting CRUD event, got report $l -> $report")
- case other =>
- throw new IllegalArgumentException(s"Expecting CRUD event, got $l -> $other")
- }
- }
- }
-
- def fromMap(from: Map[Location, Thing]): Node = {
- val ordered = from.keys.toSeq.sorted(Location)
-
- ordered.foldLeft(Node.empty) { (n, l) =>
- if(l == Root) {
- n.self = from.get(l) match {
- case Some(nd: Node) => nd.self
- case Some(t: Thing) => t
- case None => Nothing
- }
- n
- } else {
- (from(l), n ? l) match {
- case (t, NotExist) =>
- n.remember(l, Created(t))
- n
- case (t, nf: NotFound) =>
- //TODO create empty nodes from nf.missing
- n.remember(l, Created(t))
- n
- case (t, other) =>
- throw new IllegalArgumentException(s"$other not supported")
- }
- }
- }
- }
-
- def partnership(prefix: Location, ids: Seq[String]): Node = {
- new Node(relations = mutable.Map(
- partnersId -> ids.map(prefix / _)
- ))
- }
- def partnership(prefix: Location, from: Map[String, Predicate]): Node = {
- partnership(prefix, from.keys.toSeq)
- }
-
- def apply(self: Thing, leafs: Map[ID, Thing], branches: Map[ID, Node]): Node = new Node(self, mutable.Map.from(leafs), mutable.Map.from(branches))
- def apply(self: Thing, leafs: Map[ID, Thing]): Node = new Node(self, mutable.Map.from(leafs), mutable.Map.empty)
- def apply(self: Thing): Node = new Node(self, mutable.Map.empty, mutable.Map.empty)
-}
diff --git a/core/src/main/scala/dev/rudiments/hardcore/Thing.scala b/core/src/main/scala/dev/rudiments/hardcore/Thing.scala
deleted file mode 100644
index 6ccb2ba7..00000000
--- a/core/src/main/scala/dev/rudiments/hardcore/Thing.scala
+++ /dev/null
@@ -1,213 +0,0 @@
-package dev.rudiments.hardcore
-
-sealed trait Thing {}
-
-trait Agent extends Thing {
- def read(where: Location): CRUD.O
- def ?(where: Location): CRUD.O = read(where)
-
- def report(q: Query): CRUD.O
- def ?? (where: Location): CRUD.O = read(where) match {
- case Readen(n: Node) => n.report(Find(Anything))
- case other => Conflict(other, Find(Anything))
- }
- def ?* (where: Location): CRUD.O = read(where) match {
- case Readen(n: Node) => n.report(LookFor(Anything))
- case other => Conflict(other, LookFor(Anything))
- }
- def ??* (where: Location): CRUD.O = read(where) match {
- case Readen(n: Node) => n.report(Dump(Anything))
- case other => Conflict(other, Dump(Anything))
- }
-
- def !(where: Location): Link = this ? where match {
- case Readen(Node(_, _, _, relations, _, _)) => relations.get(ID("Partners")) match {
- case Some(related) => Link(where, AnyOf(related.map(l => this ! l): _*))
- }
- case Readen(p: Predicate) => Link(where, p)
- case Readen(Data(p, _)) => Link(where, p)
- case other =>
- throw new IllegalArgumentException(s"don't know $other")
- }
-}
-
-case object Internal extends Predicate
-
-final case class Link(where: Location, what: Predicate) extends Predicate {
- override def toString: String = "#" + where
-
- def data(values: Any*): Data = Data(this, values.toSeq)
-}
-final case class Declared(where: Location) extends Predicate {
- override def toString: String = "!" + where
-}
-final case class Data(what: Predicate, data: Any) extends Thing {
- override def toString: String = what match {
- case l: Link => l.toString + " {" + data.toString + "}"
- case t: Type => data match {
- case s: Seq[Any] => t.fields.map(_.name).zip(s).mkString("{", ",", "}")
- }
- case Binary =>
- data match {
- case Nothing => "Nothing"
- case arr: Seq[Byte] => "binary: " + arr.mkString("[", " ", "]")
- }
- case _ => super.toString
- }
-}
-object Data {
- val empty = Data(Nothing, Nothing)
-}
-
-sealed trait Predicate extends Thing {}
-
-final case class Type(fields: Field*) extends Predicate {
- override def toString: String = fields.mkString("{", ",", "}")
-
- def data(values: Any*): Data = Data(this, values.toSeq)
-}
-final case class Field(name: String, of: Predicate) {
- override def toString: String = name + ":" + of.toString
-} //TODO snapshot & restore for Memory[Text, Field] -> Type -> Memory[Text, Field]
-
-final case class Enlist(item: Predicate) extends Predicate {
- override def toString: String = "[" + item.toString + "]"
-}
-final case class Index(of: Predicate, over: Predicate) extends Predicate {
- override def toString: String = "{" + of.toString + "->" + over.toString + "}"
-}
-final case class AnyOf(p: Predicate*) extends Predicate {
- override def toString: String = p.mkString("*(", ",", ")")
-} // Sum-Type
-
-sealed trait Plain extends Predicate {}
-final case class Text(maxSize: Int) extends Plain
-final case class Number(from: Any, upTo: Any) extends Plain //TODO replace with more strict version
-case object Bool extends Plain {} // funny thing - in scala we can't extend object, so, or 'AnyBool' under trait, or no True and False under Bool object
-case object Binary extends Plain {} // Array[Byte]
-
-sealed trait Temporal extends Plain {}
-case object Date extends Temporal
-case object Time extends Temporal
-case object Timestamp extends Temporal
-
-case object Anything extends Predicate {}
-case object Nothing extends Predicate {}
-
-trait Message extends Thing {} //TODO separate CRUD+ from Message
-trait In extends Message {}
-trait Out extends Message {}
-trait Command extends In {}
-trait Event extends Out {}
-trait Query extends In {}
-trait Report extends Out {}
-trait Error extends Out {}
-
-sealed trait Location extends Thing {
- def / (other: Location): Location = (this, other) match {
- case (id1: ID, id2: ID) => Path(id1, id2)
- case (p: Path, id: ID) => Path((p.ids :+ id):_*)
- case (p1: Path, p2: Path) => Path((p1.ids :++ p2.ids):_*)
- case (id: ID, p: Path) => Path((id +: p.ids):_*)
- case (Root, id: ID) => id
- case (Root, p: Path) => p
- case (Root, Root) => Root
- case (p: Path, Root) => p
- case (id: ID, Root) => id
- case (a, b) => throw new IllegalArgumentException(s"prohibited $a/$b")
- }
-
- def / (p: String): Location = this./(ID(p))
- def lastString: String
-}
-object Location extends Ordering[Location] {
- def apply(s: String): Location = {
- if(s == "/") {
- Root
- } else if(!s.contains("/")) {
- ID(s)
- } else {
- this.apply(s.split("/"))
- }
- }
-
- def apply(s: Seq[String]): Location = {
- s.size match {
- case 0 => Root
- case 1 => ID(s.head)
- case _ => Path(s.map(ID): _*)
- }
- }
-
- override def compare(x: Location, y: Location): Int = (x, y) match {
- case (Root, Root) => 0
- case (Root, _: ID) => 1
- case (Root, _: Path) => 1
- case (_: ID, Root) => -1
- case (_: Path, Root) => -1
- case (idX: ID, idY: ID) => Ordering[String].compare(idX.toString, idY.toString)
- case (_: ID, _: Path) => -1
- case (_: Path, _: ID) => 1
- case (pX: Path, pY: Path) =>
- if(pX.ids.size == pY.ids.size) {
- val pairs = pX.ids.zip(pY.ids)
- pairs.foldLeft(0) { (acc, pair) =>
- if (acc == 0) {
- compare(pair._1, pair._2)
- } else {
- acc
- }
- }
- } else {
- Ordering[Int].compare(pX.ids.size, pY.ids.size)
- }
- }
-}
-final case class ID(key: Any) extends Location {
- override def toString: String = key.toString
- override def lastString: String = key.toString
-}
-final case class Path(ids: ID*) extends Location {
- override def toString: String = ids.map(_.key).mkString("/")
- override def lastString: String = ids.last.lastString
-
- def dropTail: Location = ids.size match {
- case 0 => Unmatched
- case 1 => Root // TODO exception?
- case 2 => ids.head
- case _ => Path(ids.dropRight(1):_*)
- }
-
- def head: ID = ids.head
- def tail: Location = ids.size match {
- case 0 => Unmatched
- case 1 => Root // TODO exception?
- case 2 => ids.last
- case _ => Path(ids.drop(1):_*)
- }
- def last: ID = ids.size match {
- case 0 => throw new IllegalArgumentException("Not supported empty path")
- case 1 => ids.head
- case _ => ids.last
- }
-
- def drop(other: Location): Location = {
- other match {
- case Root => this
- case id: ID if this.last == id => this.dropTail
- case p: Path =>
- val dropped = this.ids.takeRight(p.ids.size)
- if (dropped == p.ids) {
- Path(this.ids.dropRight(p.ids.size):_*)
- } else {
- Unmatched
- }
- }
- }
-}
-case object Root extends Location {
- override def lastString: String = "/"
-}
-case object Unmatched extends Location {
- override def lastString: String = "Unmatched"
-}
\ No newline at end of file
diff --git a/core/src/main/scala/dev/rudiments/hardcore/Tree.scala b/core/src/main/scala/dev/rudiments/hardcore/Tree.scala
new file mode 100644
index 00000000..29bece0e
--- /dev/null
+++ b/core/src/main/scala/dev/rudiments/hardcore/Tree.scala
@@ -0,0 +1,108 @@
+package dev.rudiments.hardcore
+
+
+case class Tree[K, B, L](
+ self: B,
+ items: Seq[(K, L | Tree[K, B, L])]
+) {
+ type Item = (List[K], L | B)
+ type Node = (K, L | Tree[K, B, L])
+ type T = Tree[K, B, L]
+ type N = L | T
+
+ val (leaves, branches) = {
+ val (ls, bs) = items.partitionMap {
+ case (id, t@Tree(_, _)) => Right[(K, L), (K, T)](id -> t.asInstanceOf[T])
+ case (id, l: L) => Left[(K, L), (K, T)](id -> l)
+ case _ => throw new IllegalStateException(s"Should never happen while indexing a tree")
+ }
+ (ls.toMap, bs.toMap)
+ }
+
+ require(
+ leaves.keySet.intersect(branches.keySet).isEmpty,
+ s"Branches and leaves have intersecting keys: ${leaves.keySet.intersect(branches.keySet)}"
+ )
+
+ val index: Map[K, N] = items.toMap
+
+
+ def read(keys: List[K]): Either[Error, N] = keys match {
+ case Nil => Right(this)
+ case h :: Nil => read(h)
+ case h :: p => this.index.get(h) match {
+ case Some(t@Tree(_, _)) => t.asInstanceOf[T].read(p)
+ case Some(l: L) => Left(LeafOnTheWay(h, p))
+ case None => Left(NotFound(keys))
+ }
+ }
+
+ def read(k: K): Either[Error, N] = this.index.get(k) match {
+ case Some(t@Tree(_, _)) => Right(t)
+ case Some(l: L) => Right(l)
+ case None => Left(NotFound(k :: Nil))
+ }
+
+ def apply(keys: List[K], evt: Event): T = ???
+
+ def apply(k: K, evt: Event): T = (this.index.get(k), evt) match {
+ case (_, Same(_)) => this //TODO conflicts
+ case (None, Created(v: N)) => new Tree(self, items :+ (k -> v))
+ case (Some(i), c@Created(_)) => throw Conflict(Readen(i), c).asException
+ case (Some(i), u@Updated(old, value)) => if(old == i) {
+ val idx = items.indexOf(k -> old)
+ if(idx == -1) { throw NotFound(k :: Nil).asException }
+ val updated = items.updated(idx, k -> value.asInstanceOf[L | Tree[K, B, L]])
+ new Tree(self, updated)
+ } else {
+ throw Conflict(Readen(i), u).asException
+ }
+ case (Some(i), d@Deleted(old)) => if(old == i) {
+ val updated = items.filterNot(item => item == (k -> old))
+ new Tree(self, updated)
+ } else {
+ throw Conflict(Readen(i), d).asException
+ }
+ case (Some(t@Tree(_, _)), Commit(events: Seq[(List[K], Event)])) =>
+ events.foldLeft(t.asInstanceOf[T]) { case (tree, (p, e)) => tree.apply(p, e)}
+ case (Some(i), evt) => throw Conflict(Readen(i), evt).asException
+ case (None, evt) => throw Conflict(NotFound(k :: Nil), evt).asException
+ }
+
+ // Search
+
+ def deep: Seq[Item] = {
+ val rootK = List.empty[K]
+ Seq(rootK -> self) ++ this.deep(rootK)
+ }
+
+ def deep(path: List[K]): Seq[Item] = {
+ items.flatMap {
+ case (k: K, t@Tree(_, _)) => Seq((path :+ k) -> t.asInstanceOf[T].self) ++ t.asInstanceOf[T].deep(path :+ k)
+ case (k: K, l: L) => Seq((path :+ k) -> l)
+ case _ => throw new IllegalStateException(s"Should never happen in deep search")
+ }
+ }
+
+
+ def wide: Seq[Item] = {
+ val rootK = List.empty[K]
+ Seq(rootK -> self) ++ this.wide(rootK)
+ }
+
+ def wide(path: List[K]): Seq[Item] = {
+ items.map {
+ case (k, t@Tree(_, _)) => (path :+ k) -> t.asInstanceOf[T].self
+ case (k, l: L) => (path :+ k) -> l
+ case _ => throw new IllegalStateException(s"Should never happen in wide search")
+ } ++ items.collect {
+ case (k, t@Tree(_, _)) => t.asInstanceOf[T].wide(path :+ k)
+ }.flatten
+ }
+}
+
+object Tree {
+ def onlyRoot[K, B, L](self: B) = new Tree(self, Seq.empty[(K, L | Tree[K, B, L])])
+ def apply[K, L](items: (K, L | Tree[K, Unit, L])*) = new Tree[K, Unit, L]((), items.toSeq)
+ def empty[K, L] = new Tree[K, Unit, L]((), Seq.empty)
+}
diff --git a/core/src/main/scala/dev/rudiments/hardcore/Tx.scala b/core/src/main/scala/dev/rudiments/hardcore/Tx.scala
deleted file mode 100644
index 09aaaea4..00000000
--- a/core/src/main/scala/dev/rudiments/hardcore/Tx.scala
+++ /dev/null
@@ -1,127 +0,0 @@
-package dev.rudiments.hardcore
-
-import dev.rudiments.hardcore.CRUD.{I, O}
-
-import scala.collection.mutable
-
-class Tx(ctx: Agent) extends AgentCrud {
- val total: mutable.Map[Location, mutable.Buffer[O]] = mutable.Map.empty
- val last: mutable.Map[Location, O] = mutable.Map.empty
-
- override def read(where: Location): O = last.get(where) match {
- case Some(Created(found)) => Readen(found)
- case Some(r: Readen) => r
- case Some(Updated(_, found)) => Readen(found)
- case Some(Deleted(_)) => NotExist
- case Some(NotExist) => NotExist
- case Some(n: NotFound) => n
- case Some(other) => throw new IllegalArgumentException(s"don't know $other")
- case None => unsafeUpdateState(where, ctx ? where)
- }
-
- override def remember(subj: Location, via: O): O = {
- (read(subj), via) match {
- case (NotExist, NotExist) => unsafeUpdateState(subj, NotExist)
- case (_: NotFound, NotExist) => unsafeUpdateState(subj, NotExist)
- case (NotExist, c: Created) => unsafeUpdateState(subj, c)
- case (NotFound(_), c: Created) => unsafeUpdateState(subj, c)
- case (NotExist, r: Readen) => unsafeUpdateState(subj, r)
- case (NotFound(_), r: Readen) => unsafeUpdateState(subj, r)
- case (Readen(found), Created(_)) => AlreadyExist(found)
- case (r@Readen(r1), Readen(r2)) if r1 == r2 => r
- case (Readen(found), Updated(u2, data)) if found == u2 => unsafeUpdateState(subj, Updated(found, data))
- case (Readen(mem: Node), Updated(u, data)) if mem.self == u => unsafeUpdateState(subj, Updated(u, data))
- case (Readen(found), Deleted(d2)) if found == d2 => unsafeUpdateState(subj, Deleted(found))
- case (found, other) =>
- Conflict(found, other)
- }
- }
-
- private def unsafeUpdateState(where: Location, what: O): O = {
- last.get(where) match {
- case Some(_) =>
- last += where -> what
- total(where) += what
- what
- case None =>
- last += where -> what
- total += where -> mutable.Buffer(what)
- what
- }
- }
-
- def verify(): O = {
- val reduced = prepare()
-
- val errors = reduced.keys.map { k =>
- val v: O = (reduced(k), last(k)) match {
- case (c@Created(c1), Created(c2)) if c1 == c2 => c
- case (u@Updated(_, u1), Updated(_, u2)) if u1 == u2 => u
- case (d@Deleted(_), Deleted(_)) => d
- case (NotExist, NotExist) => NotExist
- case (nf@NotFound(nf1), NotFound(nf2)) if nf1 == nf2 => nf
- case (r@Readen(r1), Readen(r2)) if r1 == r2 => r
- case (that, other) => Conflict(that, other)
- }
- k -> v
- }.collect { case (l, e: Error) => (l, e) }.toMap
-
- if(errors.isEmpty) {
- Valid
- } else {
- MultiError(errors)
- }
- }
-
- override def report(q: Query): O = q match {
- case Verify => this.verify()
- case Prepare =>
- this.verify() match {
- case Valid => Prepared(Commit(prepare().collect { case (l, evt: Event) => (l, evt) }))
- case other => other
- }
- case _ =>
- NotImplemented
- }
-
- def prepare(): Map[Location, O] =
- total.view.mapValues(_.toSeq.reduce(Tx.reducer(_, _))).toMap
-
- def >? : O = this.report(Verify)
- def >> : O = this.report(Prepare)
-}
-
-object Tx {
- val reducer: PartialFunction[(O, O), O] = {
- case ( NotExist, c: Created) => c
- case ( NotExist, r: Readen) => Conflict(NotExist, r)
- case ( NotExist, u: Updated) => Conflict(NotExist, u)
- case ( NotExist, d: Deleted) => Conflict(NotExist, d)
-
- case (n: NotFound, c: Created) => c
- case (n: NotFound, r: Readen) => Conflict(n, r)
- case (n: NotFound, u: Updated) => Conflict(n, u)
- case (n: NotFound, d: Deleted) => Conflict(n, d)
-
- case ( Created(c1), Created(_)) => AlreadyExist(c1)
- case ( c@Created(c1), Readen(r2)) if c1 == r2 => c
- case ( Created(c1), Updated(u1, u2)) if c1 == u1 => Created(u2)
- case ( Created(c1), d@Deleted(d2)) if c1 == d2 => NotExist
-
- case ( Readen(r1), Created(_)) => AlreadyExist(r1)
- case ( r@Readen(r1), Readen(r2)) if r1 == r2 => r
- case ( Readen(r1), u@Updated(u2, _)) if r1 == u2 => u
- case ( Readen(r1), d@Deleted(d2)) if r1 == d2 => d
-
- case ( Updated(_, u1), Created(_)) => AlreadyExist(u1)
- case ( u@Updated(_, u1), Readen(r2)) if u1 == r2 => u
- case ( Updated(u11, u12), Updated(u21, u22)) if u12 == u21 => Updated(u11, u22)
- case ( Updated(u1, u2), Deleted(d2)) if u2 == d2 => Deleted(u1)
-
- case ( Deleted(_), c: Created) => c
- case ( d:Deleted, r: Readen) => Conflict(d, r)
- case ( d:Deleted, u: Updated) => Conflict(d, u)
- case (d1:Deleted, d2: Deleted) => Conflict(d1, d2)
- case (that, other) /* unfitting updates */ => Conflict(that, other)
- }
-}
\ No newline at end of file
diff --git a/core/src/main/scala/dev/rudiments/hardcore/TypeSystem.scala b/core/src/main/scala/dev/rudiments/hardcore/TypeSystem.scala
index 5ed0fecc..399c0e50 100644
--- a/core/src/main/scala/dev/rudiments/hardcore/TypeSystem.scala
+++ b/core/src/main/scala/dev/rudiments/hardcore/TypeSystem.scala
@@ -1,76 +1,62 @@
package dev.rudiments.hardcore
-import dev.rudiments.hardcore.Node.partnersId
-
-import scala.annotation.tailrec
-
-class TypeSystem(tNode: Node) {
- val fromTypes: Map[Location, Thing] =
- tNode ??* Root match {
- case Found(_, values) => values - Root
- case other =>
- throw new IllegalStateException(s"Can't read types node, got $other")
- }
-
- val partners: Map[Location, Seq[Location]] = fromTypes.collect {
- case (l, n: Node) => l -> n.relations.getOrElse(partnersId, Seq.empty).collect {
- case p: Path => p.last
- case id: ID => id
- }
+sealed trait Thing {}
+sealed trait Predicate extends Thing {}
+object Anything extends Predicate
+object Nothing extends Predicate
+sealed trait Plain extends Predicate {}
+object Bool extends Plain
+object Number extends Plain
+object Text extends Plain
+sealed trait Temporal extends Plain {}
+object Time extends Temporal
+object Date extends Temporal
+object Timestamp extends Temporal
+
+final case class Many(of: Predicate) extends Predicate {}
+final case class Index(over: Predicate, of: Predicate) extends Predicate {}
+
+final case class PredicateRef(to: Location[String]) extends Predicate {}
+final case class ThingRef(to: Location[String]) extends Thing {}
+
+final case class Type(fields: Seq[(String, Field)]) extends Predicate {
+
+ def validate(data: Any): Either[Exception, Any] = ???
+
+ def isAbstract: Boolean = fields.exists {
+ case (_, Field.Declared(_, _)) => true
+ case (_, Field.Abstract(_, _)) => true
+ case _ => false
}
- val types: Map[Location, Type] = fromTypes.collect { case (l, t: Type) => l -> t }
- val noThings: Set[Location] = fromTypes.collect { case (l, Nothing) => l }.toSet
- val partnersRelations: Seq[(Location, Location)] =
- partners.foldLeft(Seq.empty[(Location, Location)]) { case (acc, (from, to)) =>
- acc ++ to.map(t => from -> t)
- }
- val partnersRevIndex: Map[Location, Set[Location]] =
- partnersRelations.foldLeft(Map.empty[Location, Set[Location]]) { case (acc, (from, to)) =>
- acc.get(to) match {
- case Some(s) => acc ++ Map(to -> (s + from))
- case None => acc ++ Map(to -> Set(from))
- }
- }
- val predicates: Set[Location] = partners(ID("Predicate")).toSet
- def seal(): Map[Location, Predicate] = {
- val related = partners.collect {
- case p@(_, children) if (children.toSet -- noThings -- types.keys).isEmpty => p
- }
- val adt = related.map { case (parent, children) =>
- parent -> AnyOf(
- children
- .map { l => l -> fromTypes(l) }
- .collect {
- case (l, p: Predicate) => Link(l, p)
- case (l, _) => throw new IllegalStateException(s"Not a predicate on ${l}")
- }: _*)
- }
- val complex = fromTypes -- noThings -- types.keys -- adt.keys // Message, In, Out. TODO hierarchical
- val basic: Map[Location, Predicate] = adt ++ types ++ noThings.map(l => l -> Link(l, Nothing))
- val resolved = resolveComplex(complex, basic)
+ def nonAbstract: Boolean = !isAbstract
- val diff = fromTypes -- resolved.keys
- if(diff.nonEmpty) {
- throw new IllegalStateException(s"Not all types are enum values or types or any of them: ${diff.keys.mkString(",")}")
- }
- resolved
- }
+ def make(data: Any): Data = validate(data) match
+ case Right(v) => Data(this, v)
+ case Left(err) => throw err
+}
- @tailrec
- private def resolveComplex(
- todo: Map[Location, Thing],
- known: Map[Location, Predicate]
- ): Map[Location, Predicate] = {
- val done = todo.collect { case (l, _: Node) if (partners(l).toSet -- known.keys).isEmpty =>
- l -> AnyOf(partners(l).map(p => known(p)):_*)
- }
- if(done.size < todo.size) {
- resolveComplex(todo -- done.keys, known ++ done) // could be dangerous TODO stack limit
- } else {
- known ++ done
- }
- }
+enum Field extends Thing {
+ case Value(of: Predicate, required: Boolean)
+ case Abstract(of: Predicate, required: Boolean)
+ case Method(in: Predicate, out: Predicate, f: Any => Any)
+ case Declared(in: Predicate, out: Predicate)
+}
- val typeSystem: Map[Location, Predicate] = seal()
+enum TypeEdges extends Thing {
+ case Is, Has, Extends, Realizes
+}
+
+//TODO where to put array predicates?
+enum ManyPredicates extends Predicate {
+ case Unique, Sequence
+ case Ordered(direction: OrderDirection)
+ case MaxSize(size: Long)
+ case MinSize(size: Long)
+ case ValueShould(be: Predicate)
}
+
+enum OrderDirection extends Thing:
+ case Asc, Desc
+
+final case class Data(of: Predicate, values: Any) extends Thing {}
\ No newline at end of file
diff --git a/core/src/main/scala/dev/rudiments/utils/CRC.scala b/core/src/main/scala/dev/rudiments/utils/CRC.scala
new file mode 100644
index 00000000..5a9b57a4
--- /dev/null
+++ b/core/src/main/scala/dev/rudiments/utils/CRC.scala
@@ -0,0 +1,7 @@
+package dev.rudiments.utils
+
+case class CRC(crc: Array[Byte])
+
+object CRC {
+ def apply(data: Array[Byte]): CRC = ??? // how to verify?
+}
diff --git a/core/src/main/scala/dev/rudiments/utils/Diff.scala b/core/src/main/scala/dev/rudiments/utils/Diff.scala
new file mode 100644
index 00000000..eccac337
--- /dev/null
+++ b/core/src/main/scala/dev/rudiments/utils/Diff.scala
@@ -0,0 +1,95 @@
+package dev.rudiments.utils
+
+import scala.jdk.CollectionConverters.*
+import com.github.difflib.{DiffUtils, UnifiedDiffUtils, patch}
+import com.github.difflib.patch.{AbstractDelta, ChangeDelta, DeleteDelta, DeltaType, EqualDelta, InsertDelta, Patch}
+
+case class Diff[T](
+ deltas: Seq[Delta[T]]
+) {
+ def applyTo(to: Seq[T]): Seq[T] = {
+ this.asJava.applyTo(to.asJava).asScala.toSeq
+ }
+
+ def asJava: patch.Patch[T] = {
+ val p = new Patch[T](deltas.size)
+ deltas.reverse.foreach { d => p.addDelta(d.asJava) }
+ p
+ }
+}
+
+object Diff {
+ def apply[T](from: Seq[T], to: Seq[T]): Diff[T] = {
+ fromPatch[T](DiffUtils.diff(from.asJava, to.asJava))
+ }
+
+ def fromPatch[T](patch: Patch[T]): Diff[T] = {
+ Diff(
+ patch.getDeltas.asScala.toSeq.map {
+ case d: AbstractDelta[T] if d.getType == DeltaType.INSERT =>
+ Delta.Insert(Chunk.apply[T](d.getTarget))
+ case d: AbstractDelta[T] if d.getType == DeltaType.CHANGE =>
+ Delta.Change(Chunk.apply[T](d.getSource), Chunk.apply[T](d.getTarget))
+ case d: AbstractDelta[T] if d.getType == DeltaType.DELETE =>
+ Delta.Delete(Chunk.apply[T](d.getSource))
+ case d: AbstractDelta[T] if d.getType == DeltaType.EQUAL =>
+ Delta.Equals(Chunk.apply[T](d.getSource))
+ }
+ )
+ }
+
+ def fromUnified(diff: Seq[String]): Diff[String] = {
+ Diff.fromPatch(
+ UnifiedDiffUtils.parseUnifiedDiff(diff.asJava)
+ )
+ }
+}
+
+object Unified {
+ def generate(
+ from: Seq[String], fromName: String,
+ to: Seq[String], toName: String,
+ contextSize: Int
+ ): Seq[String] = {
+ UnifiedDiffUtils.generateUnifiedDiff(
+ fromName, toName, from.asJava, Diff(from, to).asJava, contextSize
+ ).asScala.toSeq
+ }
+}
+
+enum Delta[T]:
+ def asJava: patch.AbstractDelta[T] = this match {
+ case Delta.Insert(target) => new InsertDelta[T](target.asJava, target.asJava)
+ case Delta.Change(source, target) => new ChangeDelta[T](source.asJava, target.asJava)
+ case Delta.Delete(source) => new DeleteDelta[T](source.asJava, new patch.Chunk[T](source.position, Seq.empty.asJava))
+ case Delta.Equals(source) => new EqualDelta[T](source.asJava, source.asJava)
+ }
+ case Insert(target: Chunk[T])
+ case Change(source: Chunk[T], target: Chunk[T])
+ case Delete(source: Chunk[T])
+ case Equals(source: Chunk[T])
+
+
+case class Chunk[T](
+ position: Int,
+ lines: Seq[T],
+ changes: Seq[Int] = Seq.empty
+) {
+ def asJava: patch.Chunk[T] = {
+ if(changes.isEmpty) {
+ new patch.Chunk[T](position, lines.asJava)
+ } else {
+ new patch.Chunk[T](position, lines.asJava, changes.map(scala.Int.box).asJava)
+ }
+ }
+}
+
+object Chunk {
+ def apply[T](c: patch.Chunk[T]): Chunk[T] = {
+ new Chunk[T](
+ c.getPosition,
+ c.getLines.asScala.toSeq,
+ if(c.getChangePosition == null) Seq.empty else c.getChangePosition.asScala.map(_.toInt).toSeq
+ )
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/scala/dev/rudiments/utils/Hashed.scala b/core/src/main/scala/dev/rudiments/utils/Hashed.scala
new file mode 100644
index 00000000..fb05abdf
--- /dev/null
+++ b/core/src/main/scala/dev/rudiments/utils/Hashed.scala
@@ -0,0 +1,59 @@
+package dev.rudiments.utils
+
+import java.math.BigInteger
+import java.nio.charset.StandardCharsets.UTF_8
+import java.security.MessageDigest
+import java.util.HexFormat
+import scala.collection.immutable.ArraySeq
+
+sealed trait Hashed(hash: Seq[Byte]) {
+ lazy val asArray: Array[Byte] = hash.toArray[Byte]
+ lazy val bigInteger: BigInteger = new BigInteger(1, asArray)
+ lazy val string: String = String.format("%064x", bigInteger)
+
+ override def toString: String = string
+
+ override def hashCode(): Int = this.hash.toList.hashCode()
+}
+
+object Hashed {
+ val hexFormat: HexFormat = HexFormat.of()
+}
+
+final case class SHA1(hash: Seq[Byte]) extends Hashed(hash) {
+ override lazy val string: String = String.format("%040x", bigInteger)
+}
+
+object SHA1 {
+ val digester: MessageDigest = MessageDigest.getInstance("SHA-1")
+
+ def apply(s: String): SHA1 = this.apply(s.getBytes(UTF_8))
+ def apply(b: Array[Byte]): SHA1 = new SHA1(ArraySeq.unsafeWrapArray(digester.digest(b)))
+
+ def fromHex(hex: String): SHA1 = new SHA1(ArraySeq.unsafeWrapArray(Hashed.hexFormat.parseHex(hex)))
+}
+
+final case class SHA256(hash: Seq[Byte]) extends Hashed(hash)
+
+object SHA256 {
+ val digester: MessageDigest = MessageDigest.getInstance("SHA-256")
+
+ def apply(s: String): SHA256 = this.apply(s.getBytes(UTF_8))
+ def apply(b: Array[Byte]): SHA256 = new SHA256(ArraySeq.unsafeWrapArray(digester.digest(b)))
+
+ def fromHex(hex: String): SHA256 = new SHA256(ArraySeq.unsafeWrapArray(Hashed.hexFormat.parseHex(hex)))
+}
+
+
+final case class SHA3(hash: Seq[Byte]) extends Hashed(hash)
+
+object SHA3 {
+ val digester: MessageDigest = MessageDigest.getInstance("SHA3-256")
+
+ def apply(s: String): SHA3 = this.apply(s.getBytes(UTF_8))
+ def apply(b: Array[Byte]): SHA3 = new SHA3(ArraySeq.unsafeWrapArray(digester.digest(b)))
+
+ def fromHex(hex: String): SHA3 = new SHA3(ArraySeq.unsafeWrapArray(Hashed.hexFormat.parseHex(hex)))
+
+ def empty: SHA3 = new SHA3(ArraySeq.unsafeWrapArray(digester.digest(Array.empty[Byte])))
+}
diff --git a/core/src/main/scala/dev/rudiments/utils/Log.scala b/core/src/main/scala/dev/rudiments/utils/Log.scala
new file mode 100644
index 00000000..418fb554
--- /dev/null
+++ b/core/src/main/scala/dev/rudiments/utils/Log.scala
@@ -0,0 +1,7 @@
+package dev.rudiments.utils
+
+import org.slf4j.{Logger, LoggerFactory}
+
+trait Log {
+ lazy val log: Logger = LoggerFactory.getLogger(this.getClass)
+}
diff --git a/core/src/main/scala/dev/rudiments/utils/ZLib.scala b/core/src/main/scala/dev/rudiments/utils/ZLib.scala
new file mode 100644
index 00000000..5915476a
--- /dev/null
+++ b/core/src/main/scala/dev/rudiments/utils/ZLib.scala
@@ -0,0 +1,42 @@
+package dev.rudiments.utils
+
+import java.io.ByteArrayOutputStream
+import java.util.zip.{Deflater, Inflater}
+
+object ZLib {
+ val DEFAULT_BUFFER_SIZE = 4096
+
+ def pack(data: Array[Byte], size: Int = DEFAULT_BUFFER_SIZE): Array[Byte] = {
+ val deflater = new Deflater()
+ deflater.setLevel(Deflater.DEFAULT_COMPRESSION)
+ deflater.setInput(data)
+
+ val outputStream = new ByteArrayOutputStream(data.length)
+ try {
+ deflater.finish()
+ val buffer = new Array[Byte](size)
+ while (!deflater.finished) {
+ val count = deflater.deflate(buffer)
+ outputStream.write(buffer, 0, count)
+ }
+ outputStream.toByteArray
+ } finally
+ if (outputStream != null) outputStream.close()
+ }
+
+ def unpack(data: Array[Byte], size: Int = DEFAULT_BUFFER_SIZE): Array[Byte] = {
+ val inflater = new Inflater()
+ inflater.setInput(data)
+
+ val outputStream = new ByteArrayOutputStream(data.length)
+ try {
+ val buffer = new Array[Byte](size)
+ while (!inflater.finished) {
+ val count = inflater.inflate(buffer)
+ outputStream.write(buffer, 0, count)
+ }
+ outputStream.toByteArray
+ } finally
+ if (outputStream != null) outputStream.close()
+ }
+}
diff --git a/core/src/test/scala/test/dev/rudiments/Sample.scala b/core/src/test/scala/test/dev/rudiments/Sample.scala
new file mode 100644
index 00000000..15022c33
--- /dev/null
+++ b/core/src/test/scala/test/dev/rudiments/Sample.scala
@@ -0,0 +1,9 @@
+package test.dev.rudiments
+
+case class Sample(
+ a: Int,
+ b: String,
+ c: Seq[String]
+)
+
+case class Example(i: Int, s: Sample)
\ No newline at end of file
diff --git a/core/src/test/scala/test/dev/rudiments/Smt.scala b/core/src/test/scala/test/dev/rudiments/Smt.scala
deleted file mode 100644
index 3806d7e6..00000000
--- a/core/src/test/scala/test/dev/rudiments/Smt.scala
+++ /dev/null
@@ -1,13 +0,0 @@
-package test.dev.rudiments
-
-sealed trait Blah {}
-
-case class Smt(
- id: Long,
- name: String,
- comment: Option[String]
-) extends Blah
-
-case class Thng(
- code: String
-) extends Blah
\ No newline at end of file
diff --git a/core/src/test/scala/test/dev/rudiments/codecs/CirceTest.scala b/core/src/test/scala/test/dev/rudiments/codecs/CirceTest.scala
new file mode 100644
index 00000000..aa876f05
--- /dev/null
+++ b/core/src/test/scala/test/dev/rudiments/codecs/CirceTest.scala
@@ -0,0 +1,27 @@
+package test.dev.rudiments.codecs
+
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+import io.circe.generic.semiauto.*
+import io.circe.{ Codec, Json }
+import test.dev.rudiments.Sample
+
+class CirceTest extends AnyWordSpec with Matchers {
+ implicit val codec: Codec[Sample] = Codec.from(deriveDecoder[Sample], deriveEncoder[Sample])
+ private val sample = Sample(42, "the answer", Seq("to", "life", "the", "universe", "and", "everything"))
+
+ "can encode custom class" in {
+ codec(sample) should be (Json.obj(
+ "a" -> Json.fromInt(42),
+ "b" -> Json.fromString("the answer"),
+ "c" -> Json.arr(
+ Json.fromString("to"),
+ Json.fromString("life"),
+ Json.fromString("the"),
+ Json.fromString("universe"),
+ Json.fromString("and"),
+ Json.fromString("everything")
+ )
+ ))
+ }
+}
diff --git a/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala b/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala
new file mode 100644
index 00000000..9a041702
--- /dev/null
+++ b/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala
@@ -0,0 +1,61 @@
+package test.dev.rudiments.codecs
+
+import dev.rudiments.codecs.Result.*
+import dev.rudiments.codecs.{ MJ, MirrorInfo, TS }
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+import test.dev.rudiments.{ Example, Sample }
+
+import scala.compiletime.{ constValue, erasedValue, error, summonFrom }
+
+class CodecTest extends AnyWordSpec with Matchers {
+
+ "can generate Codecs graph from the Type graph" in {
+ //TODO Type Graph with custom types
+ }
+
+ "can encode primitive types" in {
+ MJ.strToText.t("hello!") should be (Ok(TS.Text("hello!")))
+ MJ.intToNumber.t(42) should be (Ok(TS.Number(42)))
+ }
+
+ "can encode list of primitive types" in {
+ import dev.rudiments.codecs.MJ.given
+
+ MJ.many(using MJ.intToNumber).t(Seq(0, 1, 1, 2, 3, 5)) should be(Ok(TS.Many(Seq(
+ TS.Number(0), TS.Number(1), TS.Number(1), TS.Number(2), TS.Number(3), TS.Number(5)
+ ))))
+ }
+
+ "can encode a map string -> number" in {
+ import dev.rudiments.codecs.MJ.given
+
+ MJ.index(using strToText, intToNumber).t(Map(
+ "1" -> 1,
+ "2" -> 2,
+ "42" -> 42,
+ "24" -> 24
+ )) should be (Ok(TS.Idx(Map(
+ TS.Text("1") -> TS.Number(1),
+ TS.Text("2") -> TS.Number(2),
+ TS.Text("42") -> TS.Number(42),
+ TS.Text("24") -> TS.Number(24),
+ ))))
+ }
+
+ "can derive int and string fields of a case class and recursively" in {
+ val sampleShouldBe = MirrorInfo[Sample]("Sample", Seq(
+ "a" -> MirrorInfo.intInfo,
+ "b" -> MirrorInfo.strInfo,
+ "c" -> MirrorInfo.seqInfo[String]
+ ))
+ MirrorInfo[Sample] should be (sampleShouldBe)
+
+ MirrorInfo[Example] should be(
+ MirrorInfo[Example]("Example", Seq(
+ "i" -> MirrorInfo.intInfo,
+ "s" -> sampleShouldBe
+ ))
+ )
+ }
+}
diff --git a/core/src/test/scala/test/dev/rudiments/hardcore/BulkTest.scala b/core/src/test/scala/test/dev/rudiments/hardcore/BulkTest.scala
deleted file mode 100644
index dbc2970f..00000000
--- a/core/src/test/scala/test/dev/rudiments/hardcore/BulkTest.scala
+++ /dev/null
@@ -1,53 +0,0 @@
-package test.dev.rudiments.hardcore
-
-import dev.rudiments.hardcore._
-import org.junit.Ignore
-import org.junit.runner.RunWith
-import org.scalatest.matchers.should.Matchers
-import org.scalatest.wordspec.AnyWordSpec
-import org.scalatestplus.junit.JUnitRunner
-
-@RunWith(classOf[JUnitRunner])
-class BulkTest extends AnyWordSpec with Matchers {
- private val sampleSize: Int = 1*1000*10 // TODO 1*1000*1000 == 1M, now fails because of commit.getHash collision
- private val rFill = Range(1, sampleSize + 1)
- private val rRead = Range(sampleSize + 1, 1)
-
- private val ctx: Memory = new Memory()
- private val tx: Tx = new Tx(ctx)
-
-
- private val t = Type(Field("i", Number(0, sampleSize)), Field("j", Text(10)), Field("k", Text(0)))
-
- private var commit: Commit = _
-
- s"can create Commit with $sampleSize records" in {
- rFill.foreach { i =>
- tx += ID(i) -> Data(t, Seq(i, i.toString, ""))
- }
- commit = tx.>>.asInstanceOf[Prepared].commit
- commit.crud.size should be (sampleSize)
- }
-
- "can update Memory with big Commit" in {
- ctx << commit
- rRead.foreach { i =>
- ctx ? ID(i) should be (Readen(Data(t, Seq(i, i.toString, ""))))
- }
- }
-
- "can update every item in memory" in {
- rFill.foreach { i =>
- withClue(s"i: $i") {
- val localTx = new Tx(ctx)
- localTx.remember(ID(i), Updated(
- Data(t, Seq(i, i.toString, "")),
- Data(t, Seq(-i, "!" + i.toString, "!")),
- ))
- val cmt = localTx.>>.asInstanceOf[Prepared].commit
- ctx << cmt should be(Committed(cmt))
- ctx ? ID(i) should be(Readen(Data(t, Seq(-i, "!" + i.toString, "!"))))
- }
- }
- }
-}
diff --git a/core/src/test/scala/test/dev/rudiments/hardcore/GraphTest.scala b/core/src/test/scala/test/dev/rudiments/hardcore/GraphTest.scala
new file mode 100644
index 00000000..21abba7f
--- /dev/null
+++ b/core/src/test/scala/test/dev/rudiments/hardcore/GraphTest.scala
@@ -0,0 +1,12 @@
+package test.dev.rudiments.hardcore
+
+import dev.rudiments.hardcore.Graph
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+
+class GraphTest extends AnyWordSpec with Matchers {
+
+ "can create empty graph" in {
+ Graph.empty[Int, Int, Int] should be(Graph[Int, Int, Int](Map.empty, Seq.empty))
+ }
+}
diff --git a/core/src/test/scala/test/dev/rudiments/hardcore/LocationSpec.scala b/core/src/test/scala/test/dev/rudiments/hardcore/LocationSpec.scala
deleted file mode 100644
index 6b88f9a0..00000000
--- a/core/src/test/scala/test/dev/rudiments/hardcore/LocationSpec.scala
+++ /dev/null
@@ -1,42 +0,0 @@
-package test.dev.rudiments.hardcore
-
-import dev.rudiments.hardcore._
-import org.junit.runner.RunWith
-import org.scalatest.matchers.should.Matchers
-import org.scalatest.wordspec.AnyWordSpec
-import org.scalatestplus.junit.JUnitRunner
-
-@RunWith(classOf[JUnitRunner])
-class LocationSpec extends AnyWordSpec with Matchers {
- private val id1 = ID("42")
- private val id2 = ID("35")
- private val id3 = ID("57")
- private val id4 = ID("13")
- private val id5 = ID("5")
-
- private val paths: Map[Location, Thing] = Map(
- id5 -> Nothing,
- Root -> Nothing,
- id1 -> Node.empty, //is it ok?
- id1 / id2 -> Nothing,
- id1 / id3 -> Nothing,
- id1 / id4 -> Nothing,
- )
-
- "can build hierarchical Node from paths" in {
- Node.fromMap(paths) should be (Node(
- Nothing,
- Map(id5 -> Nothing),
- Map(
- id1 -> Node(
- Nothing,
- Map(
- id2 -> Nothing,
- id3 -> Nothing,
- id4 -> Nothing
- )
- )
- )
- ))
- }
-}
diff --git a/core/src/test/scala/test/dev/rudiments/hardcore/MemorySpec.scala b/core/src/test/scala/test/dev/rudiments/hardcore/MemorySpec.scala
deleted file mode 100644
index 5894f4df..00000000
--- a/core/src/test/scala/test/dev/rudiments/hardcore/MemorySpec.scala
+++ /dev/null
@@ -1,29 +0,0 @@
-package test.dev.rudiments.hardcore
-
-import dev.rudiments.hardcore._
-import org.junit.runner.RunWith
-import org.scalatest.matchers.should.Matchers
-import org.scalatest.wordspec.AnyWordSpec
-import org.scalatestplus.junit.JUnitRunner
-
-@RunWith(classOf[JUnitRunner])
-class MemorySpec extends AnyWordSpec with Matchers {
- private val ctx: Memory = new Memory()
-
- private val id: Location = ID("42")
- private val t = Type(Field("a", Bool))
- private val data = Data(t, Seq(true))
- private val data2 = Data(t, Seq(false))
-
- "NotExist until something Created" in { ctx ? id should be (NotExist) }
- "can Create if NotExist" in { ctx + (id, data) should be (Created(data)) }
- "can remember Created" in { (ctx += id -> data) should be (Created(data)) }
- "can Read if Created" in { ctx ? id should be (Readen(data)) }
- "can Update if Created" in { ctx * (id, data2) should be (Updated(data, data2)) }
- "can Delete if Created" in { ctx - id should be (Deleted(data)) }
- "can remember Updated" in { (ctx *= id -> data2) should be (Updated(data, data2)) }
- "can Read if Updated" in { ctx ? id should be (Readen(data2)) }
- "can Delete if Updated" in { ctx - id should be (Deleted(data2)) }
- "can remember Deleted" in { (ctx -= id) should be (Deleted(data2)) }
- "NotExist if Deleted" in { ctx ? id should be (NotExist) }
-}
diff --git a/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala b/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala
deleted file mode 100644
index 182c3939..00000000
--- a/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala
+++ /dev/null
@@ -1,28 +0,0 @@
-package test.dev.rudiments.hardcore
-
-import dev.rudiments.hardcore._
-import org.junit.runner.RunWith
-import org.scalatest.matchers.should.Matchers
-import org.scalatest.wordspec.AnyWordSpec
-import org.scalatestplus.junit.JUnitRunner
-
-@RunWith(classOf[JUnitRunner])
-class NodeSpec extends AnyWordSpec with Matchers {
- private val mem: Node = Node.empty
- private val id: Location = ID("42")
- private val t = Type(Field("a", Bool))
- private val data = Data(t, Seq(true))
- private val data2 = Data(t, Seq(false))
-
- "NotExist until something Created" in { mem ? id should be (NotExist) }
- "can Create if NotExist" in { mem + (id, data) should be (Created(data)) }
- "can remember Created" in { (mem += id -> data) should be (Created(data)) }
- "can Read if Created" in { mem ? id should be (Readen(data)) }
- "can Update if Created" in { mem * (id, data2) should be (Updated(data, data2)) }
- "can Delete if Created" in { mem - id should be (Deleted(data)) }
- "can remember Updated" in { (mem *= id -> data2) should be (Updated(data, data2)) }
- "can Read if Updated" in { mem ? id should be (Readen(data2)) }
- "can Delete if Updated" in { mem - id should be (Deleted(data2)) }
- "can remember Deleted" in { (mem -= id) should be (Deleted(data2)) }
- "NotExist if Deleted" in { mem ? id should be (NotExist) }
-}
diff --git a/core/src/test/scala/test/dev/rudiments/hardcore/TreeTest.scala b/core/src/test/scala/test/dev/rudiments/hardcore/TreeTest.scala
new file mode 100644
index 00000000..175db930
--- /dev/null
+++ b/core/src/test/scala/test/dev/rudiments/hardcore/TreeTest.scala
@@ -0,0 +1,201 @@
+package test.dev.rudiments.hardcore
+
+import dev.rudiments.hardcore.{ Created, Deleted, LeafOnTheWay, NotFound, Tree, Updated }
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+
+class TreeTest extends AnyWordSpec with Matchers {
+ "can make empty tree" in {
+ val t = Tree.empty[Int, Int]
+ t.self should be (())
+ t.items shouldBe empty
+ }
+
+ var t: Tree[Int, Unit, String] = _
+ "can make nested trees with leaves" in {
+ t = Tree(
+ 1 -> "a", 2 -> "b",
+ 3 -> Tree(
+ 4 -> "c",
+ 5 -> Tree(
+ 6 -> "d",
+ 7 -> "e"
+ ),
+ 8 -> Tree(
+ 9 -> "f"
+ ),
+ 10 -> "g"
+ ),
+ 11 -> "h"
+ )
+
+ t.self should be(())
+ t.items.size should be(4)
+ }
+
+ "can read from a nested trees" in {
+ t.read(1 :: Nil) should be (Right("a"))
+ t.read(3 :: 4 :: Nil) should be (Right("c"))
+ t.read(3 :: 8 :: 9 :: Nil) should be (Right("f"))
+
+ t.read(3 :: 4 :: 5 :: Nil) should be (Left(LeafOnTheWay(4, 5 :: Nil)))
+ t.read(42 :: Nil) should be (Left(NotFound(42 :: Nil)))
+
+ t.read(3 :: 8 :: Nil) should be (Right(Tree(
+ 9 -> "f"
+ )))
+ t.read(3 :: Nil) should be(Right(Tree(
+ 4 -> "c",
+ 5 -> Tree(
+ 6 -> "d",
+ 7 -> "e"
+ ),
+ 8 -> Tree(
+ 9 -> "f"
+ ),
+ 10 -> "g"
+ )))
+ t.read(Nil) should be (Right(Tree(
+ 1 -> "a", 2 -> "b",
+ 3 -> Tree(
+ 4 -> "c",
+ 5 -> Tree(
+ 6 -> "d",
+ 7 -> "e"
+ ),
+ 8 -> Tree(
+ 9 -> "f"
+ ),
+ 10 -> "g"
+ ),
+ 11 -> "h"
+ )))
+ }
+
+ "can make a deep search in nested trees" in {
+ t.deep should be(Seq(
+ List.empty[Int] -> (),
+ List(1) -> "a", List(2) -> "b",
+ List(3) -> (),
+ List(3, 4) -> "c",
+ List(3, 5) -> (),
+ List(3, 5, 6) -> "d",
+ List(3, 5, 7) -> "e",
+ List(3, 8) -> (),
+ List(3, 8, 9) -> "f",
+ List(3, 10) -> "g",
+ List(11) -> "h"
+ ))
+ }
+
+ "can make a wide search in nested trees" in {
+ t.wide should be (Seq(
+ List.empty[Int] -> (),
+ List(1) -> "a", List(2) -> "b",
+ List(3) -> (),
+ List(11) -> "h",
+ List(3, 4) -> "c",
+ List(3, 5) -> (),
+ List(3, 8) -> (),
+ List(3, 10) -> "g",
+ List(3, 5, 6) -> "d",
+ List(3, 5, 7) -> "e",
+ List(3, 8, 9) -> "f",
+ ))
+ }
+
+ "can create element" in {
+ t.read(12 :: Nil) should be (Left(NotFound(12 :: Nil)))
+
+ t = t.apply(12, Created("k"))
+
+ t should be (Tree(
+ 1 -> "a", 2 -> "b",
+ 3 -> Tree(
+ 4 -> "c",
+ 5 -> Tree(
+ 6 -> "d",
+ 7 -> "e"
+ ),
+ 8 -> Tree(
+ 9 -> "f"
+ ),
+ 10 -> "g"
+ ),
+ 11 -> "h", 12 -> "k"
+ ))
+
+ t.read(13 :: Nil) should be (Left(NotFound(13 :: Nil)))
+
+ t = t.apply(13, Created(Tree(14 -> "l")))
+
+ t should be(Tree(
+ 1 -> "a", 2 -> "b",
+ 3 -> Tree(
+ 4 -> "c",
+ 5 -> Tree(
+ 6 -> "d",
+ 7 -> "e"
+ ),
+ 8 -> Tree(
+ 9 -> "f"
+ ),
+ 10 -> "g"
+ ),
+ 11 -> "h", 12 -> "k",
+ 13 -> Tree(14 -> "l")
+ ))
+ }
+
+ "can update element" in {
+ t.read(12) should be (Right("k"))
+ t = t.apply(12, Updated("k", "j"))
+ t.read(12) should be (Right("j"))
+
+ t.read(13) should be (Right(Tree( 14 -> "l" )))
+ t = t.apply(13, Updated(
+ Tree( 14 -> "l" ),
+ Tree( 15 -> Tree( 16 -> "m" ), 17 -> "n" ))
+ )
+ t.read(13) should be (Right(Tree(
+ 15 -> Tree( 16 -> "m" ),
+ 17 -> "n"
+ )))
+ t should be (Tree(
+ 1 -> "a", 2 -> "b",
+ 3 -> Tree(
+ 4 -> "c",
+ 5 -> Tree( 6 -> "d", 7 -> "e" ),
+ 8 -> Tree( 9 -> "f" ),
+ 10 -> "g"
+ ),
+ 11 -> "h", 12 -> "j",
+ 13 -> Tree(
+ 15 -> Tree( 16 -> "m" ),
+ 17 -> "n"
+ )
+ ))
+ }
+
+ "can delete element" in {
+ t = t.apply(12, Deleted("j"))
+ t.read(12) should be (Left(NotFound(12 :: Nil)))
+
+ t = t.apply(13, Deleted(Tree(
+ 15 -> Tree( 16 -> "m" ),
+ 17 -> "n"
+ )))
+ t.read(13) should be (Left(NotFound(13 :: Nil)))
+
+ t should be(Tree(
+ 1 -> "a", 2 -> "b",
+ 3 -> Tree(
+ 4 -> "c",
+ 5 -> Tree(6 -> "d", 7 -> "e"),
+ 8 -> Tree(9 -> "f"),
+ 10 -> "g"
+ ),
+ 11 -> "h"
+ ))
+ }
+}
diff --git a/core/src/test/scala/test/dev/rudiments/hardcore/TxSpec.scala b/core/src/test/scala/test/dev/rudiments/hardcore/TxSpec.scala
deleted file mode 100644
index c171c072..00000000
--- a/core/src/test/scala/test/dev/rudiments/hardcore/TxSpec.scala
+++ /dev/null
@@ -1,95 +0,0 @@
-package test.dev.rudiments.hardcore
-
-import dev.rudiments.hardcore._
-import org.junit.runner.RunWith
-import org.scalatest.matchers.should.Matchers
-import org.scalatest.wordspec.AnyWordSpec
-import org.scalatestplus.junit.JUnitRunner
-
-@RunWith(classOf[JUnitRunner])
-class TxSpec extends AnyWordSpec with Matchers {
- private val ctx: Memory = new Memory()
- private val tx: Tx = new Tx(ctx)
-
- private val id = ID("42")
-
- private val t = Type(Field("a", Bool))
- private val data = Data(t, Seq(true))
- private val data2 = Data(t, Seq(false))
-
- private val initialCommit = ctx ??* ID("commits") match {
- case Found(_, values) => values - Root
- case other =>
- fail("Can't read initial commits")
- }
-
- "can't read non-existing ID in Tx" in { tx ? ID("not exist") should be (NotExist) }
-
- "can remember Created" in {
- ctx << Commit(Map(id -> Created(data)))
- }
- "can Read if Created" in { ctx ? id should be (Readen(data)) }
-
- "can read Created in Tx as Readen" in { tx ? id should be (Readen(data)) }
-
- "can modify in Tx without modification in Context" in {
- (tx *= id -> data2) should be (Updated(data, data2))
- tx ? id should be (Readen(data2))
-
- ctx ? id should be (Readen(data))
- }
-
- "can Delete in Tx" in {
- (tx -= id) should be (Deleted(data2))
- tx ? id should be (NotExist)
-
- ctx ? id should be (Readen(data))
- }
-
- "can Verify Tx" in {
- tx.>? should be (Valid)
- }
-
- "can Prepare Change from Tx" in {
- tx.>> should be (Prepared(Commit(Map(id -> Deleted(data)))))
- }
-
- "can Change Context" in {
- tx.>> match {
- case Prepared(cmt) => ctx << cmt should be (Committed(cmt))
- }
- }
-
- "can see commits of Context" in {
- val found = ctx ?? ID("commits")
- val expected = Found(Find(Anything), initialCommit ++ Map(
- ID("1240340089") -> Commit(Map(id -> Created(data))),
- ID("-847544541") -> Commit(Map(id -> Deleted(data)))
- ))
- found should be (expected)
- }
-
- "can prepare commit comparing memory" in {
- val to = Node(Nothing,
- Map(
- ID("example-true") -> Data(Bool, true),
- ID("example-false") -> Data(Bool, false)
- ),
- Map(
- ID("nested-1") -> Node(Nothing,
- Map(ID("inside") -> Data(Text(20), "content of inside"))
- ),
- ID("nested-2") -> Node(Nothing,
- Map.empty[ID, Thing],
- Map(ID("nested-3") -> Node(Nothing,
- Map(ID("deep-inside") -> Data(Number(0, 100), 42))
- ))
- )
- ),
- )
-
- val compared = Node.empty.reconcile(to)
- val recreated = Node.fromEventMap(compared)
- recreated should be (to)
- }
-}
diff --git a/core/src/test/scala/test/dev/rudiments/hardcore/TypeSystemSpec.scala b/core/src/test/scala/test/dev/rudiments/hardcore/TypeSystemSpec.scala
deleted file mode 100644
index caed6abc..00000000
--- a/core/src/test/scala/test/dev/rudiments/hardcore/TypeSystemSpec.scala
+++ /dev/null
@@ -1,24 +0,0 @@
-package test.dev.rudiments.hardcore
-
-import dev.rudiments.hardcore._
-import org.junit.runner.RunWith
-import org.scalatest.matchers.should.Matchers
-import org.scalatest.wordspec.AnyWordSpec
-import org.scalatestplus.junit.JUnitRunner
-
-@RunWith(classOf[JUnitRunner])
-class TypeSystemSpec extends AnyWordSpec with Matchers {
- private val mem = new Memory()
- private val tNode = mem /! Initial.types
-
- "can make TypeSystem from /types node" in {
- val ts = new TypeSystem(tNode)
- ts.types.size should be (30)
- ts.noThings.size should be (18)
- }
-
- "can seal type system" in {
- val ts = new TypeSystem(tNode)
- ts.seal().size should be (62)
- }
-}
diff --git a/core/src/test/scala/test/dev/rudiments/utils/DiffTest.scala b/core/src/test/scala/test/dev/rudiments/utils/DiffTest.scala
new file mode 100644
index 00000000..5ae558d5
--- /dev/null
+++ b/core/src/test/scala/test/dev/rudiments/utils/DiffTest.scala
@@ -0,0 +1,54 @@
+package test.dev.rudiments.utils
+
+import com.github.difflib.DiffUtils
+import dev.rudiments.utils.{Chunk, Delta, Diff, Unified}
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+
+import scala.jdk.CollectionConverters.*
+
+class DiffTest extends AnyWordSpec with Matchers {
+ private val text1 = Seq("This is a test senctence.", "This is the second line.", "And here is the finish.")
+ private val text2 = Seq("This is a test for diffutils.", "This is the second line.")
+
+ "diff wrapping" in {
+ val diff = Diff(text1, text2)
+
+ diff should be(
+ Diff(Seq(
+ Delta.Change(Chunk(0, Seq("This is a test senctence.")), Chunk(0, Seq("This is a test for diffutils."))),
+ Delta.Delete(Chunk(2, Seq("And here is the finish.")))
+ ))
+ )
+ }
+
+ "wrapping should produce similar to lib output" in {
+ val diff = Diff(text1, text2)
+ diff.applyTo(text1) should be (text2)
+ }
+
+ "can make universal diff" in {
+ val unified = Unified.generate(text1, "text1", text2, "text2", 0)
+ unified should be (Seq(
+ "--- text1", "+++ text2",
+ "@@ -1,1 +1,1 @@", "-This is a test senctence.", "+This is a test for diffutils.",
+ "@@ -3,1 +3,0 @@", "-And here is the finish."
+ ))
+ }
+
+ "can restore diff from unified format" in {
+ val unified = Unified.generate(text1, "text1", text2, "text2", 0)
+ Diff.fromUnified(unified) should be (
+ Diff(Seq(
+ Delta.Change(
+ Chunk(0, Seq("This is a test senctence."), Seq(1)),
+ Chunk(0, Seq("This is a test for diffutils."), Seq(1))
+ ),
+ Delta.Change(
+ Chunk(2, Seq("And here is the finish."), Seq(3)),
+ Chunk(2, Seq.empty[String], Seq.empty) //TODO merge as Delta.Delete
+ )
+ ))
+ )
+ }
+}
diff --git a/core/src/test/scala/test/dev/rudiments/utils/HashedTest.scala b/core/src/test/scala/test/dev/rudiments/utils/HashedTest.scala
new file mode 100644
index 00000000..378d16f3
--- /dev/null
+++ b/core/src/test/scala/test/dev/rudiments/utils/HashedTest.scala
@@ -0,0 +1,51 @@
+package test.dev.rudiments.utils
+
+import dev.rudiments.utils.{SHA256, SHA3, SHA1}
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+
+class HashedTest extends AnyWordSpec with Matchers {
+ "SHA-1 hash" should {
+ "fit with known hashes" in {
+ val known = Map(
+ "" -> "da39a3ee5e6b4b0d3255bfef95601890afd80709",
+ "The quick brown fox jumps over the lazy dog" -> "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"
+ )
+
+ val hashed = known.map((k, _) => k -> SHA1(k).toString)
+ hashed should be(known)
+ }
+
+ "can be encoded and decoded from HEX" in {
+ val h = SHA1("The quick brown fox jumps over the lazy dog")
+ h.toString should be ("2fd4e1c67a2d28fced849ee1bb76e7391b93eb12")
+ SHA1.fromHex("2fd4e1c67a2d28fced849ee1bb76e7391b93eb12") should be (h)
+ }
+ }
+
+ "SHA-256 hash" should {
+ "fit with known hashes" in {
+ val known = Map(
+ "" -> "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "sha-256" -> "3128f8ac2988e171a53782b144b98a5c2ee723489c8b220cece002916fbc71e2",
+ "The quick brown fox jumps over the lazy dog" -> "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592"
+ )
+
+ val hashed = known.map((k, _) => k -> SHA256(k).toString)
+ hashed should be (known)
+ }
+ }
+
+ "SHA3-256 hash" should {
+ "fit with known hashes" in {
+ val known = Map(
+ "" -> "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a",
+ "sha3-256" -> "5d90e98d57bb0f24f935080cb1bab85eaedec5d958fa979cd53e8147e32111e1",
+ "The quick brown fox jumps over the lazy dog" -> "69070dda01975c8c120c3aada1b282394e7f032fa9cf32f4cb2259a0897dfc04"
+ )
+
+ val hashed = known.map((k, _) => k -> SHA3(k).toString)
+ hashed should be(known)
+ }
+ }
+}
diff --git a/example/build.gradle b/example/build.gradle
index 01a00ab9..748412e3 100644
--- a/example/build.gradle
+++ b/example/build.gradle
@@ -1,23 +1,15 @@
+plugins {
+ id 'dev.rudiments.scala-app-conventions'
+}
+
dependencies {
implementation project(':core')
- implementation project(':http')
implementation project(':file')
- implementation project(':management')
-
- implementation 'io.circe:circe-core_2.13:0.14.1'
- implementation 'io.circe:circe-generic_2.13:0.14.1'
- implementation 'io.circe:circe-generic-extras_2.13:0.14.1'
-
- implementation 'de.heikoseeberger:akka-http-circe_2.13:1.38.2'
-
- implementation 'ch.qos.logback:logback-classic:1.2.7'
+ implementation project(':git')
- testImplementation 'com.typesafe.akka:akka-testkit_2.13:2.6.17'
- testImplementation 'com.typesafe.akka:akka-http-testkit_2.13:10.2.7'
+ implementation 'ch.qos.logback:logback-classic:1.4.14'
}
-task example(type: JavaExec, dependsOn: classes) {
- classpath sourceSets.main.runtimeClasspath
+application {
mainClass = 'dev.rudiments.app.Main'
- standardInput = System.in
}
\ No newline at end of file
diff --git a/example/example-file.http b/example/example-file.http
deleted file mode 100644
index b3d04198..00000000
--- a/example/example-file.http
+++ /dev/null
@@ -1,34 +0,0 @@
-GET http://localhost:8080/api/file
-Accept: application/json
-
-###
-
-GET http://localhost:8080/api/file/
-Accept: application/json
-
-###
-
-GET http://localhost:8080/api/file/docker-compose.yml
-Accept: application/json
-
-###
-
-GET http://localhost:8080/api/file/research
-Accept: application/json
-
-###
-
-GET http://localhost:8080/api/file/research/
-Accept: application/json
-
-###
-
-GET http://localhost:8080/api/file/research/agents.md
-Accept: application/json
-
-###
-
-GET http://localhost:8080/api/file/example/src/main/scala/dev/rudiments/app/Main.scala
-Accept: application/json
-
-###
\ No newline at end of file
diff --git a/example/example.http b/example/example.http
deleted file mode 100644
index fb0b617d..00000000
--- a/example/example.http
+++ /dev/null
@@ -1,31 +0,0 @@
-GET http://localhost:8080/api/example
-Accept: application/json
-
-###
-
-GET http://localhost:8080/api/example/42
-Accept: application/json
-
-###
-
-POST http://localhost:8080/api/example/13
-Content-Type: application/json
-
-{
- "a": true
-}
-
-###
-
-GET http://localhost:8080/api/example/some-data
-Accept: application/json
-
-###
-
-PUT http://localhost:8080/api/example/some-data
-Content-Type: application/json
-
-{
- "name": "some else data",
- "strings": ["blah", "blah-blah", "blah-blah-blah-blah!"]
-}
diff --git a/example/routers.http b/example/routers.http
deleted file mode 100644
index 109a8bcc..00000000
--- a/example/routers.http
+++ /dev/null
@@ -1,17 +0,0 @@
-GET http://localhost:8080/api/routers/
-Accept: application/json
-
-###
-
-GET http://localhost:8080/api/routers
-Accept: application/json
-
-###
-
-GET http://localhost:8080/api/routers/types
-Accept: application/json
-
-###
-
-GET http://localhost:8080/api/all/
-Accept: application/json
\ No newline at end of file
diff --git a/example/src/main/scala/dev/rudiments/app/Main.scala b/example/src/main/scala/dev/rudiments/app/Main.scala
index 47d97b58..162f68c7 100644
--- a/example/src/main/scala/dev/rudiments/app/Main.scala
+++ b/example/src/main/scala/dev/rudiments/app/Main.scala
@@ -1,39 +1,7 @@
package dev.rudiments.app
-import akka.actor.ActorSystem
-import com.typesafe.config.ConfigFactory
-import dev.rudiments.hardcore.Initial.types
-import dev.rudiments.hardcore._
-import dev.rudiments.management.Management
-import dev.rudiments.hardcore.file.FileAgent
-import dev.rudiments.hardcore.http.{RootRouter, ScalaRouter, ThingDecoder}
-import io.circe.Decoder
+import dev.rudiments.utils.Log
-object Main extends App {
- private implicit val actorSystem: ActorSystem = ActorSystem()
-
- private val mem: Memory = new Memory()
- Management.init(mem.node)
- private val ts = new TypeSystem(mem /! types)
- private val td: ThingDecoder = new ThingDecoder(ts)
-
- val files = ID("files")
- // mem += files -> Node.empty
- // mem /! files << uploadFiles
-
- private val router = new ScalaRouter(mem.node)(td).routes
- new RootRouter(
- RootRouter.config(ConfigFactory.load()),
- "api" -> router
- ).bind()
-
- private def uploadFiles: Commit = {
- val fileAgent = new FileAgent(".")
- fileAgent.reconsFor(mem /! files) match {
- case Prepared(cmt) =>
- cmt
- case _ =>
- throw new IllegalStateException("Unexpected result of load")
- }
- }
+object Main extends App with Log {
+ log.info("Main app")
}
diff --git a/example/src/test/scala/test/dev/rudiments/app/BoardSpec.scala b/example/src/test/scala/test/dev/rudiments/app/BoardSpec.scala
deleted file mode 100644
index b3cc8bef..00000000
--- a/example/src/test/scala/test/dev/rudiments/app/BoardSpec.scala
+++ /dev/null
@@ -1,136 +0,0 @@
-package test.dev.rudiments.app
-
-import akka.actor.ActorSystem
-import akka.http.scaladsl.testkit.ScalatestRouteTest
-import dev.rudiments.hardcore.Initial.types
-import dev.rudiments.hardcore._
-import dev.rudiments.hardcore.http.{CirceSupport, ScalaRouter, ThingDecoder}
-import dev.rudiments.management.Management
-import io.circe.{Decoder, Json}
-import org.junit.runner.RunWith
-import org.scalatest.Inside.inside
-import org.scalatest.matchers.should.Matchers
-import org.scalatest.wordspec.AnyWordSpec
-import org.scalatestplus.junit.JUnitRunner
-
-import java.sql
-import scala.collection.mutable
-
-@RunWith(classOf[JUnitRunner])
-class BoardSpec extends AnyWordSpec with Matchers with ScalatestRouteTest with CirceSupport {
- private implicit val actorSystem: ActorSystem = ActorSystem()
- private val mem = new Memory()
- Management.init(mem.node)
- private val ts = new TypeSystem(mem /! types)
- private val td = new ThingDecoder(ts)
-
- private val n: Node = mem /! Management.boards
- private val router = new ScalaRouter(n)(td)
- private val routes = router.seal()
- private val nodeDe = td.anythingDecoder
- private val c = mem ! (types / "BoardColumn")
- private val t = mem ! (types / "Task")
-
- private val board1: Thing = Node(leafIs = c, leafs = mutable.Map(
- ID("column-1") -> c.data(Seq.empty),
- ID("column-2") -> c.data(Seq(
- Management.tasks / "task-1",
- Management.tasks / "task-2"
- )),
- ID("column-3") -> c.data(Seq.empty)
- ))
-
- private val tx = new Tx(mem.node)
- //TODO move init data into files
- tx += Management.tasks / "task-1" -> t.data(
- "task-1",
- "summ of task #1",
- sql.Date.valueOf("2022-06-06"),
- Link(types / "TODO", Nothing)
- )
- tx += Management.tasks / "task-2" -> t.data(
- "task-2",
- "summ of task #2",
- sql.Date.valueOf("2022-05-07"),
- Link(types / "InProgress", Nothing)
- )
- tx += Management.tasks / "task-3" -> t.data(
- "task-3",
- "summ of task #3",
- sql.Date.valueOf("2022-04-08"),
- Link(types / "Done", Nothing)
- )
-
- tx += Management.boards / "board-1" -> board1
-
- "can prepare tx" in {
- tx.>> match {
- case Prepared(c1) =>
- mem.node << c1 match {
- case Committed(c2) => c2
- case other =>
- fail(s"Failed to commit Tx: $other")
- }
- case other =>
- fail(s"Failed to prepare Tx: $other")
- }
- }
-
- "can encode board" in {
- router.thingEncoder(board1) should be(Json.obj(
- "type" -> Json.fromString("Node"),
- "key-is" -> Json.obj(
- "type" -> Json.fromString("Text"),
- "max-size" -> Json.fromString("1024")
- ),
- "leaf-is" -> Json.obj("type" -> Json.fromString("BoardColumn")),
- "leafs" -> Json.obj(
- "column-1" -> Json.obj("tasks" -> Json.arr()),
- "column-2" -> Json.obj("tasks" -> Json.arr(
- Json.fromString((Management.tasks / "task-1").toString),
- Json.fromString((Management.tasks / "task-2").toString),
- )),
- "column-3" -> Json.obj("tasks" -> Json.arr())
- )
- ))
- }
-
- "can decode location" in {
- val json = Json.obj(
- "type" -> Json.fromString("Path"),
- "ids" -> Json.fromString((Management.tasks / "task-1").toString)
- )
-
- nodeDe.decodeJson(json) should be (Right(Management.tasks / "task-1"))
- }
-
- "can decode single column" in {
- val json = Json.obj(
- "type" -> Json.fromString("BoardColumn"),
- "tasks" -> Json.arr(
- Json.fromString((Management.tasks / "task-1").toString),
- Json.fromString((Management.tasks / "task-2").toString)
- )
- )
- nodeDe.decodeJson(json) should be (Right(
- c.data(Seq(Location("work/tasks/task-1"), Location("work/tasks/task-2")))
- ))
- }
-
- "can decode board" in {
- val encoded = router.thingEncoder(board1)
- val decoded = nodeDe.decodeJson(encoded)
- inside(decoded) {
- case Right(n: Node) =>
- n.self should be (Nothing)
- n.keyIs should be (Text(1024))
- n.leafIs should be (c)
- n.leafs.size should be (3)
- n.leafs.get(ID("column-2")) should be (Some(c.data(Seq(
- Management.tasks / "task-1",
- Management.tasks / "task-2"
- ))))
- n should be (board1)
- }
- }
-}
diff --git a/example/src/test/scala/test/dev/rudiments/app/CheckTest.scala b/example/src/test/scala/test/dev/rudiments/app/CheckTest.scala
new file mode 100644
index 00000000..907565e3
--- /dev/null
+++ b/example/src/test/scala/test/dev/rudiments/app/CheckTest.scala
@@ -0,0 +1,11 @@
+package test.dev.rudiments.app
+
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+
+class CheckTest extends AnyWordSpec with Matchers {
+ "always true" in {
+ val a = true
+ a should be(true)
+ }
+}
diff --git a/example/src/test/scala/test/dev/rudiments/app/TasksSpec.scala b/example/src/test/scala/test/dev/rudiments/app/TasksSpec.scala
deleted file mode 100644
index c65a4717..00000000
--- a/example/src/test/scala/test/dev/rudiments/app/TasksSpec.scala
+++ /dev/null
@@ -1,108 +0,0 @@
-package test.dev.rudiments.app
-
-import akka.actor.ActorSystem
-import akka.http.scaladsl.model.StatusCodes
-import akka.http.scaladsl.testkit.ScalatestRouteTest
-import dev.rudiments.hardcore.Initial.types
-import dev.rudiments.hardcore._
-import dev.rudiments.hardcore.http.{CirceSupport, ScalaRouter, ThingDecoder}
-import dev.rudiments.management.Management
-import io.circe.{Decoder, Json}
-import org.junit.Ignore
-
-import java.sql
-import org.junit.runner.RunWith
-import org.scalatest.matchers.should.Matchers
-import org.scalatest.wordspec.AnyWordSpec
-import org.scalatestplus.junit.JUnitRunner
-
-@RunWith(classOf[JUnitRunner])
-class TasksSpec extends AnyWordSpec with Matchers with ScalatestRouteTest with CirceSupport {
- private implicit val actorSystem: ActorSystem = ActorSystem()
- private val mem = new Memory()
- Management.init(mem.node)
- private val ts = new TypeSystem(mem /! types)
- private val td = new ThingDecoder(ts)
-
- private val n: Node = mem /! Management.tasks
- private val router = new ScalaRouter(n)(td)
- private val routes = router.seal()
- private val t = mem ? (types / "Task") match {
- case Readen(found: Type) => found
- case other => fail(s"unexpected read of types/Task: $other")
- }
- private implicit val de: Decoder[Thing] = td.decoder(t).map(_.asInstanceOf[Data])
-
-// mem /! Management.team += ID("alice") -> Management.userLink.data("Alice", "alice@test.org")
-
- private val sample: Thing = t.data(
- "task-1",
- "summ of task #1",
- sql.Date.valueOf("2022-06-06"),
- Link(types / "InProgress", Nothing)
- )
- private val sample2: Thing = t.data(
- "task-2",
- "summ of task #2",
- sql.Date.valueOf("2022-05-07"),
- Link(types / "TODO", Nothing)
- )
-
- "can encode task" in {
- router.thingEncoder(sample) should be (Json.obj(
- "name" -> Json.fromString("task-1"),
- "summary" -> Json.fromString("summ of task #1"),
- "deadline" -> Json.fromString("2022-06-06"),
- "status" -> Json.fromString("InProgress")
- ))
- }
-
- "can decode task" in {
- td.dataTypeDecoder(t).decodeJson(Json.obj(
- "name" -> Json.fromString("task-1"),
- "summary" -> Json.fromString("summ of task #1"),
- "deadline" -> Json.fromString("2022-06-06"),
- "status" -> Json.fromString("InProgress")
- )) should be (Right(sample))
- }
-
- "create task" in {
- n ? ID("42") should be (NotExist)
- Post("/42", sample) ~> routes ~> check {
- response.status should be (StatusCodes.Created)
- responseAs[Thing] should be (sample)
- }
- n ? ID("42") should be (Readen(sample))
-
- Get("/42") ~> routes ~> check {
- response.status should be (StatusCodes.OK)
- responseAs[Thing] should be (sample)
- }
- }
-
- "update task" in {
- Put("/42", sample2) ~> routes ~> check {
- response.status should be (StatusCodes.OK)
- responseAs[Thing] should be (sample2)
- }
- Get("/42") ~> routes ~> check {
- response.status should be (StatusCodes.OK)
- responseAs[Thing] should be (sample2)
- }
- }
-
- "can't create second task with same ID" in {
- Post("/42", sample2) ~> routes ~> check {
- response.status should be (StatusCodes.Conflict)
- }
- }
-
- "delete task" in {
- Delete("/42") ~> routes ~> check {
- response.status should be (StatusCodes.NoContent)
- }
- Get("/42") ~> routes ~> check {
- response.status should be (StatusCodes.NotFound)
- }
- }
-}
diff --git a/example/types.http b/example/types.http
deleted file mode 100644
index a4a83426..00000000
--- a/example/types.http
+++ /dev/null
@@ -1,22 +0,0 @@
-GET http://localhost:8080/api/types/
-Accept: application/json
-
-###
-
-GET http://localhost:8080/api/types
-Accept: application/json
-
-###
-
-GET http://localhost:8080/api/types/relations/
-Accept: application/json
-
-###
-
-GET http://localhost:8080/api/types/Type
-Accept: application/json
-
-###
-
-GET http://localhost:8080/api/types/Field
-Accept: application/json
diff --git a/file/build.gradle b/file/build.gradle
index 0ed26752..c5c6a61e 100644
--- a/file/build.gradle
+++ b/file/build.gradle
@@ -1,8 +1,7 @@
+plugins {
+ id 'dev.rudiments.scala-lib-conventions'
+}
+
dependencies {
implementation project(':core')
- implementation project(':http')
-
- implementation 'io.circe:circe-core_2.13:0.14.1'
- implementation 'io.circe:circe-generic_2.13:0.14.1'
- implementation 'io.circe:circe-generic-extras_2.13:0.14.1'
-}
\ No newline at end of file
+}
diff --git a/file/src/main/scala/dev/rudiments/hardcore/file/File.scala b/file/src/main/scala/dev/rudiments/hardcore/file/File.scala
deleted file mode 100644
index c265bcf7..00000000
--- a/file/src/main/scala/dev/rudiments/hardcore/file/File.scala
+++ /dev/null
@@ -1,29 +0,0 @@
-package dev.rudiments.hardcore.file
-
-import dev.rudiments.hardcore._
-
-import scala.collection.Seq
-
-sealed trait File {}
-object File {
- val folder: Link = Link(ID("Folder"), Nothing)
- val textFile: Link = Link(ID("TextFile"), Nothing)
- val unknownFile: Link = Link(ID("UnknownFile"), Nothing)
-
- val file: Predicate = AnyOf(folder, textFile, unknownFile)
-}
-
-case object Folder extends File {
- val typeOf: Predicate = Index(Text(Int.MaxValue), File.file)
-}
-
-case object TextFile extends File {
- val typeOf: Predicate = Enlist(Text(Int.MaxValue))
-
- val textFileExtensions: Seq[String] = Seq(".txt", ".scala", ".java", ".gradle", ".yml", ".sql", ".md", ".conf", ".xml", ".http", ".json")
- def isTextFile(name: String): Boolean = textFileExtensions.exists(name.endsWith)
-}
-
-case object UnknownFile extends File {
- val empty: Data = Data(Binary, Nothing)
-}
\ No newline at end of file
diff --git a/file/src/main/scala/dev/rudiments/hardcore/file/FileAgent.scala b/file/src/main/scala/dev/rudiments/hardcore/file/FileAgent.scala
deleted file mode 100644
index 64f09df8..00000000
--- a/file/src/main/scala/dev/rudiments/hardcore/file/FileAgent.scala
+++ /dev/null
@@ -1,248 +0,0 @@
-package dev.rudiments.hardcore.file
-
-import dev.rudiments.hardcore.CRUD.{Evt, O}
-import dev.rudiments.hardcore._
-import dev.rudiments.hardcore.http.ThingEncoder
-
-import java.io.{FileWriter, File => JavaFile}
-import java.lang
-import java.nio.charset.Charset
-import java.nio.file.{Files, Paths}
-import scala.collection.mutable
-import scala.io.Source
-import scala.util.Using
-
-class FileAgent(absolutePath: String) {
- def everything(prefix: Location = Root): Map[Location, Thing] = {
- read(prefix) match {
- case Readen(d@Data(Folder.typeOf, folders: Map[ID, File])) =>
- val s = Map[Location, Thing](prefix -> Node(d))
- val files = folders.keySet.flatMap { k => everything(prefix / k) }.toMap
- s ++ files
- case Readen(d: Data) =>
- Map[Location, Thing](prefix -> d)
- case other =>
- throw new IllegalArgumentException(s"Unexpected read result of $absolutePath/$prefix}")
- }
- }
-
- def read(where: Location): Out = where match {
- case Root => readFile(absolutePath)
- case id: ID => readFile(absolutePath + "/" + id.key.toString)
- case path: Path => readFile(absolutePath + "/" + path.toString)
- case other => throw new IllegalArgumentException(s"Unsupported: $other")
- }
-
- def compose(where: Location): Thing = {
- read(where) match {
- case Readen(d@Data(Folder.typeOf, folders: Map[ID, File])) =>
- val loaded = folders.map { case (k, _) => k -> compose(where / k) }
- val errors = loaded.collect { case p@(_, _: Error) => p }
-
- if(errors.isEmpty) {
- val branches: Map[ID, Node] = loaded.collect { case (id, m: Node) => id -> m }
- val data: Map[ID, Thing] = loaded.collect {
- case (_, _: Out) => None //TODO more invalid options
- case (_, _: Node) => None
- case p@(_, _) => Some(p)
- }.flatten.toMap
- Node(
- d,
- data,
- branches
- )
- } else {
- MultiError(errors.asInstanceOf[Map[Location, Error]])
- }
- case Readen(d: Data) => d
- case other => throw new IllegalStateException(s"Where '$other' coming from?")
- }
- }
-
- def reconsFor(mem: Node): Out = {
- compose(Root) match {
- case err: Error => err
- case out: Out => out
- case m: Node =>
- val compared = mem.reconcile(m)
- val changes = compared.collect { case (l, evt: Evt) => l -> evt }
- val errors = compared.collect { case (l, err: Error) => l -> err }
- if (errors.isEmpty && changes.nonEmpty) {
- if (changes.size == 1 && changes.contains(Root)) {
- changes(Root)
- } else {
- Prepared(Commit(changes))
- }
- } else if (changes.isEmpty) {
- Identical
- } else {
- MultiError(errors)
- }
- case t: Thing =>
- (t, mem.self) match {
- case (d, Nothing) => Created(d)
- case (Nothing, Nothing) => Readen(Nothing)
- case (d, s) if d != s => Updated(s, d)
- case (Nothing, s) => Deleted(s)
- }
- }
- }
-
- def reconcile(to: Node): Map[Location, O] = {
- val source = to.everything()
- val target = this.everything()
- val keys = (source.keySet ++ target.keySet).toSeq.sorted(Location)
-
- keys.map { k =>
- (source.get(k), target.get(k)) match {
- case (None, None) => throw new IllegalStateException("How this happen?")
- case (None, Some(incoming)) => k -> Created(incoming)
- case (Some(existing), Some(incoming)) if existing == incoming => k -> Identical
- case (Some(existing), Some(incoming)) if existing != incoming => k -> Updated(existing, incoming)
- case (Some(existing), None) => k -> Deleted(existing)
- }
- }.toMap
- }
-
- def readFile(path: String): Out = {
- val f = new JavaFile(path)
- if(!f.exists()) {
- NotExist
- } else {
- if(f.isDirectory) {
- Readen(Data(Folder.typeOf,
- f.listFiles().toSeq.collect {
- case f: JavaFile if f.isDirectory => ID(f.getName) -> File.folder
- case f: JavaFile if f.isFile && TextFile.isTextFile(f.getName) => ID(f.getName) -> File.textFile
- case f: JavaFile if f.isFile => ID(f.getName) -> File.unknownFile
- }.toMap
- ))
- } else if (f.isFile) {
- if(TextFile.isTextFile(f.getName)) {
- Using(Source.fromFile(path)) { f =>
- Readen(Data(TextFile.typeOf, f.getLines().toSeq))
- }.getOrElse(FileError(s"Failed to read $path"))
- } else {
- Readen(Data(Binary, Files.readAllBytes(Paths.get(path)).toSeq))
- }
- } else {
- FileError(s"Unknown file ${f.getAbsolutePath}")
- }
- }
- }
-
- def writeFileFromNode(node: Node, where: Location): Out = { //TODO File events?
- if(where == Root) { //will return AlreadyExist if directory already exist
- mkDir(absolutePath)
- } else {
- mkDir(absolutePath + "/" + where)
- }
-
- val branches = node.branches.map { case (id, n) =>
- //TODO if all leafs and branches Deleted ?
- mkDir(absolutePath + "/" + (where / id).toString)
- id -> writeFileFromNode(n, where / id)
- }
-
- val leafs = node.leafs.map { //TODO more checks on update and delete?
- case (id, Created(data)) => id -> writeFile((where / id).toString, data)
- case (id, Updated(_, data)) => id -> writeFile((where / id).toString, data)
- case (id, Deleted(_)) => id -> deleteFile((where / id).toString)
- case (id, c: Commit) => id -> writeFile((where / id).toString, c)
- case (id, data: Data) => id -> writeFile((where / id).toString, data)
- case (id, Nothing) => id -> writeFile((where / id).toString, Nothing)
- case (id, _) =>
- id -> NotImplemented
- }
-
- val errors = leafs ++ branches filter {
- case (_, _: Error) => true
- case _ => false
- }
-
- if(errors.nonEmpty) {
- MultiError(errors.toMap)
- } else {
- WrittenTextFile(Data.empty)
- }
- }
-
- def mkDir(path: String): Out = {
- val f = new JavaFile(path)
- try {
- if(!f.exists()) {
- f.mkdir()
- Created(Data.empty)
- } else {
- AlreadyExist(Data.empty) //TODO read dir for info?
- }
- } catch {
- case e: Exception => FileError(e.getMessage)
- }
- }
-
- def writeFile(path: String, content: Any): Out = {
- def wf(f: JavaFile): Out = {
- content match {
- case d@Data(TextFile.typeOf, content: Seq[String]) =>
- f.createNewFile()
- val writer = new FileWriter(f, Charset.defaultCharset())
- try {
- content.foreach { s =>
- writer.write(s)
- writer.write("\n")
- }
- Created(d)
- } finally {
- writer.close()
- }
- case Data(Binary, Nothing) =>
- f.createNewFile()
- Created(Nothing)
- case d@Data(Binary, _: Seq[Byte]) =>
- f.createNewFile()
- Created(d)
- case cmt: Commit =>
- f.createNewFile()
- val writer = new FileWriter(f, Charset.defaultCharset())
- try {
- writer.write(ThingEncoder.encodeOut(Prepared(cmt)).toString())
- Created(cmt)
- } finally {
- writer.close()
- }
- case Nothing =>
- f.createNewFile()
- Created(Nothing)
- case _ =>
- NotImplemented
- }
- }
-
- val f = new JavaFile(absolutePath + "/" + path)
- try {
- if(!f.exists()) {
- wf(f)
- } else {
- f.delete()
- wf(f)
- }
- } catch {
- case e: Exception => FileError(e.getMessage)
- }
- }
-
- def deleteFile(path: String): Out = {
- val f = new JavaFile(absolutePath + "/" + path)
- try {
- if(!f.exists()) {
- NotExist
- } else {
- f.delete()
- Deleted(Data.empty)
- }
- } catch {
- case e: Exception => FileError(e.getMessage)
- }
- }
-}
diff --git a/file/src/main/scala/dev/rudiments/hardcore/file/Messages.scala b/file/src/main/scala/dev/rudiments/hardcore/file/Messages.scala
deleted file mode 100644
index 17b31df3..00000000
--- a/file/src/main/scala/dev/rudiments/hardcore/file/Messages.scala
+++ /dev/null
@@ -1,22 +0,0 @@
-package dev.rudiments.hardcore.file
-
-import dev.rudiments.hardcore.{Command, Data, Error, Event, ID}
-
-sealed trait FileSystemIO {}
-case object ClearCache extends Command with FileSystemIO
-
-sealed trait DirIO extends FileSystemIO {}
-case object ReadStructure extends Command with DirIO
-case class ReadenStructure(files: Map[ID, File]) extends Event with DirIO
-
-
-sealed trait FileIO extends FileSystemIO {}
-case object ReadFile extends Command with FileIO
-case class ReadenTextFile(lines: Data) extends Event with FileIO
-case class ReadenBinaryFile(content: Data) extends Event with FileIO
-
-case class WriteTextFile(lines: Data) extends Command with FileIO
-case class WrittenTextFile(content: Data) extends Event with FileIO
-
-
-case class FileError(message: String) extends Error with FileSystemIO
\ No newline at end of file
diff --git a/file/src/test/resources/application.conf b/file/src/test/resources/application.conf
deleted file mode 100644
index 1ff245df..00000000
--- a/file/src/test/resources/application.conf
+++ /dev/null
@@ -1,16 +0,0 @@
-http {
- prefix = "api"
-
- host = "localhost"
- port = 8080
-
- akka-http-cors {
- allow-generic-http-requests = true
- allow-credentials = true
- allowed-origins = ["*"]
- allowed-headers = ["*"]
- allowed-methods = ["GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE", "UPGRADE"]
- exposed-headers = ["X-Request-ID"]
- max-age = 30 m
- }
-}
\ No newline at end of file
diff --git a/file/src/test/resources/example/1.txt b/file/src/test/resources/example/1.txt
new file mode 100644
index 00000000..d4b4f36f
--- /dev/null
+++ b/file/src/test/resources/example/1.txt
@@ -0,0 +1 @@
+first file
\ No newline at end of file
diff --git a/file/src/test/resources/example/nested/2.txt b/file/src/test/resources/example/nested/2.txt
new file mode 100644
index 00000000..0f8bbfe7
--- /dev/null
+++ b/file/src/test/resources/example/nested/2.txt
@@ -0,0 +1 @@
+second file
\ No newline at end of file
diff --git a/file/src/test/resources/file-test/24.bin b/file/src/test/resources/file-test/24.bin
deleted file mode 100644
index c2f29ec7..00000000
--- a/file/src/test/resources/file-test/24.bin
+++ /dev/null
@@ -1 +0,0 @@
-unknown file example
\ No newline at end of file
diff --git a/file/src/test/resources/file-test/42.json b/file/src/test/resources/file-test/42.json
deleted file mode 100644
index fe9ee2c9..00000000
--- a/file/src/test/resources/file-test/42.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "a": true
-}
\ No newline at end of file
diff --git a/file/src/test/resources/file-test/folder1/folder2/123.json b/file/src/test/resources/file-test/folder1/folder2/123.json
deleted file mode 100644
index 648de0dd..00000000
--- a/file/src/test/resources/file-test/folder1/folder2/123.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "b": false
-}
\ No newline at end of file
diff --git a/file/src/test/resources/logback.xml b/file/src/test/resources/logback.xml
deleted file mode 100644
index a994bdfc..00000000
--- a/file/src/test/resources/logback.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- %d{HH:mm:ss.SSS} %-5level %-20logger{20} %msg%n
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/file/src/test/scala/test/dev/rudiments/hardcore/file/FileSpec.scala b/file/src/test/scala/test/dev/rudiments/hardcore/file/FileSpec.scala
deleted file mode 100644
index 56f5c79c..00000000
--- a/file/src/test/scala/test/dev/rudiments/hardcore/file/FileSpec.scala
+++ /dev/null
@@ -1,153 +0,0 @@
-package test.dev.rudiments.hardcore.file
-
-import dev.rudiments.hardcore.CRUD.Evt
-import dev.rudiments.hardcore._
-import dev.rudiments.hardcore.file._
-import org.junit.runner.RunWith
-import org.scalatest.matchers.should.Matchers
-import org.scalatest.wordspec.AnyWordSpec
-import org.scalatestplus.junit.JUnitRunner
-
-@RunWith(classOf[JUnitRunner])
-class FileSpec extends AnyWordSpec with Matchers {
- private val filePath = "src/test/resources/file-test"
- private val outFilePath = "build/tmp/test-files"
- private val files = ID("files")
- private val fileAgent = new FileAgent(filePath)
- private val ctx: Memory = new Memory
-
- private val initialFound = ctx ??* Root match {
- case Found(_, values) =>
- values
- case _ => fail("Can't read initial memory state")
- }
-
- ctx += files -> Node.empty
-
- private val commitEvents: Map[Location, CRUD.Evt] = Map(
- Root -> Updated(Node.empty, Node(Data(Folder.typeOf, Map(
- ID("folder1") -> File.folder,
- ID("24.bin") -> File.unknownFile,
- ID("42.json") -> File.textFile
- )))),
-
- ID("folder1") -> Created(Node(Data(Folder.typeOf, Map(ID("folder2") -> File.folder)))),
- ID("24.bin") -> Created(Data(Binary, Seq[Byte](117, 110, 107, 110, 111, 119, 110, 32, 102, 105, 108, 101, 32, 101, 120, 97, 109, 112, 108, 101))),
- ID("42.json") -> Created(Data(TextFile.typeOf, Seq("{", " \"a\": true", "}"))),
-
- ID("folder1") / ID("folder2") -> Created(Node(Data(Folder.typeOf, Map(ID("123.json") -> File.textFile)))),
- ID("folder1") / ID("folder2") / ID("123.json") -> Created(Data(TextFile.typeOf, Seq("{", " \"b\": false", "}"))),
- )
- private val commitMemory: Node = Node(
- Data(Folder.typeOf, Map(
- ID("folder1") -> File.folder,
- ID("24.bin") -> File.unknownFile,
- ID("42.json") -> File.textFile
- )),
- Map(
- ID("24.bin") -> Data(Binary, Seq[Byte](117, 110, 107, 110, 111, 119, 110, 32, 102, 105, 108, 101, 32, 101, 120, 97, 109, 112, 108, 101)),
- ID("42.json") -> Data(TextFile.typeOf, Seq("{", " \"a\": true", "}")),
- ),
- Map(
- ID("folder1") -> Node(
- Data(Folder.typeOf, Map(ID("folder2") -> File.folder)),
- Map.empty[ID, Thing],
- Map(
- ID("folder2") -> Node(
- Data(Folder.typeOf, Map(ID("123.json") -> File.textFile)),
- Map(
- ID("123.json") -> Data(TextFile.typeOf, Seq("{", " \"b\": false", "}"))
- )
- )
- )
- )
- )
- )
-
- "can read file in Agent" in {
- val readen = fileAgent.read(Root)
- readen should be (
- Readen(
- Data(
- Folder.typeOf,
- Map(
- ID("folder1") -> File.folder,
- ID("24.bin") -> File.unknownFile,
- ID("42.json") -> File.textFile
- )
- )
- )
- )
- }
-
- "can prepare commit with memory" in {
- val loaded = fileAgent.everything(Root)
- val node = Node.fromMap(loaded)
- node should be(commitMemory)
- }
-
- "can read everything" in {
- val loaded = fileAgent.everything()
- loaded should be(Map(
- Root -> Node(Data(Folder.typeOf, Map(
- ID("folder1") -> File.folder,
- ID("24.bin") -> File.unknownFile,
- ID("42.json") -> File.textFile
- ))),
-
- ID("folder1") -> Node(Data(Folder.typeOf, Map(ID("folder2") -> File.folder))),
- ID("24.bin") -> Data(Binary, Seq[Byte](117, 110, 107, 110, 111, 119, 110, 32, 102, 105, 108, 101, 32, 101, 120, 97, 109, 112, 108, 101)),
- ID("42.json") -> Data(TextFile.typeOf, Seq("{", " \"a\": true", "}")),
-
- ID("folder1") / ID("folder2") -> Node(Data(Folder.typeOf, Map(ID("123.json") -> File.textFile))),
- ID("folder1") / ID("folder2") / ID("123.json") -> Data(TextFile.typeOf, Seq("{", " \"b\": false", "}"))
- ))
- }
-
- "can prepare commit via Agent" in {
- val out = fileAgent.reconcile(ctx /! files)
- out.foreach { case (l, evt) =>
- withClue(s"location: $l") {
- commitEvents.get(l) match {
- case Some(found) =>
- evt should be (found)
- case None => fail("Not found")
- }
- }
- }
- out should be (commitEvents)
- }
-
- "can save prepared into Context" in {
- val out = fileAgent.reconcile(ctx /! files)
- val crud = out.collect { case (l, evt: Evt) => l -> evt }
- val cmt = Commit(crud)
- val result = ctx.remember(files, Committed(cmt))
- result should be (Committed(cmt))
-
- ctx ??* files match {
- case Found(_, values) =>
- val expecting = commitMemory.everything()
- values.keySet.toSeq.sorted(Location).foreach { k =>
- withClue(s"comparing location: $k") {
- values(k) should be(expecting(k))
- }
- }
- case other => fail("expecting Found All")
- }
- }
-
- "can write Commit into files elsewhere" in {
- val otherFile = new FileAgent(outFilePath)
- val node = Node.fromEventMap(commitEvents)
- otherFile.writeFileFromNode(node, Root) should be (WrittenTextFile(Data.empty))
- }
-
- "can write commit into json file" in {
- val otherFile = new FileAgent(outFilePath)
- ctx ? Memory.commits match {
- case Readen(node: Node) => otherFile.writeFileFromNode(node, Root)
- case other => fail("Expecting initial commit")
- }
- }
-}
diff --git a/git/build.gradle b/git/build.gradle
new file mode 100644
index 00000000..8b79d288
--- /dev/null
+++ b/git/build.gradle
@@ -0,0 +1,8 @@
+plugins {
+ id 'dev.rudiments.scala-lib-conventions'
+}
+
+dependencies {
+ implementation project(':core')
+ implementation project(':file')
+}
diff --git a/git/src/main/scala/dev/rudiments/git/ByteUtils.scala b/git/src/main/scala/dev/rudiments/git/ByteUtils.scala
new file mode 100644
index 00000000..fedbd34a
--- /dev/null
+++ b/git/src/main/scala/dev/rudiments/git/ByteUtils.scala
@@ -0,0 +1,75 @@
+package dev.rudiments.git
+
+import java.nio.ByteBuffer
+
+implicit class ByteBufferOps(buff: ByteBuffer) {
+ def unsigned(): Int = buff.get() & 0xFF
+ def getUBytes(n: Int): Seq[Byte] = {
+ val arr = new Array[Byte](n)
+ buff.get(arr)
+ arr.toSeq
+ }
+}
+
+implicit class ByteUtils(b: Byte) {
+ def mask(c: Byte): Boolean = (b & c) != 0
+ def bitIsSet(bit: Int): Boolean = {
+ if(bit > 7 || bit < 0) throw new IllegalArgumentException(s"Not supported bit position $bit")
+ else this.mask((1 << bit).toByte)
+ }
+}
+
+implicit class IntForByteUtils(i: Int) {
+ def mask(c: Int): Boolean = (i & c) != 0
+ def bitIsSet(bit: Int): Boolean = {
+ if (bit > 7 || bit < 0) throw new IllegalArgumentException(s"Not supported bit position $bit")
+ else this.mask(1 << bit)
+ }
+}
+
+object ByteUtils {
+ def variableSize(buff: ByteBuffer): Int = {
+ var cursor = 0
+ var size: Int = 0
+
+ while {
+ val b = buff.get()
+ size = size | ((b & 0x7F) << cursor)
+ cursor += 7
+
+ nextIsSize(b) && cursor <= 32
+ } do ()
+
+ size
+ }
+
+ //TODO variable size with PackObject type
+
+ def delta(buff: ByteBuffer): Deltified = {
+ val head = buff.unsigned()
+ if(head.bitIsSet(7)) {
+ var offset = 0
+ if(head.bitIsSet(0)) offset = buff.unsigned()
+ if(head.bitIsSet(1)) offset |= (buff.unsigned() << 8)
+ if(head.bitIsSet(2)) offset |= (buff.unsigned() << 16)
+ if(head.bitIsSet(3)) offset |= (buff.unsigned() << 24)
+
+ var size = 0
+ if(head.bitIsSet(4)) size = buff.unsigned()
+ if(head.bitIsSet(5)) size |= (buff.unsigned() << 8)
+ if(head.bitIsSet(6)) size |= (buff.unsigned() << 16)
+
+ if(size == 0) size = 0x10000
+
+ Deltified.Copy(offset, size)
+ } else {
+ if(head != 0) {
+ Deltified.Add(head, buff.getUBytes(head))
+ } else {
+ throw new IllegalArgumentException("Not supported 0000 0000 byte")
+ }
+ }
+ }
+
+ def nextIsSize(b: Byte): Boolean = (b & 0x80).toByte == -128.toByte
+}
diff --git a/git/src/main/scala/dev/rudiments/git/GitObject.scala b/git/src/main/scala/dev/rudiments/git/GitObject.scala
new file mode 100644
index 00000000..01d1413e
--- /dev/null
+++ b/git/src/main/scala/dev/rudiments/git/GitObject.scala
@@ -0,0 +1,261 @@
+package dev.rudiments.git
+
+import dev.rudiments.git.Commit.Field.{ Author, Parent }
+import dev.rudiments.utils.{ SHA1, ZLib }
+
+import java.lang
+import java.lang.IllegalStateException
+import java.nio.ByteBuffer
+import java.nio.charset.StandardCharsets.UTF_8
+import java.nio.file.Path
+import java.time.format.{ DateTimeFormatter, DateTimeFormatterBuilder, SignStyle }
+import java.time.temporal.ChronoField
+import java.time.{ Instant, ZoneId, ZonedDateTime }
+import scala.collection.mutable
+import scala.util.matching.Regex
+
+sealed trait GitObject(kind: String) {
+ val header: String = s"$kind $size"
+ def data: Array[Byte]
+ def full: Array[Byte] = (header.getBytes(UTF_8) :+ 0.toByte) ++ data
+ def hash: SHA1 = SHA1(full)
+ def size: Int = data.length
+
+ def validate(size: Int, hash: String): Either[Exception, this.type] =
+ if (this.size != size) Left(new IllegalStateException(s"Invalid size, expected: ${this.size}"))
+ else if (this.hash.toString != hash) Left(new IllegalStateException(s"Invalid hash, expected: ${this.hash}"))
+ else Right(this)
+
+ def objectPath: Path = {
+ val subDir = hash.toString.take(2)
+ val fileName = hash.toString.drop(2)
+ Path.of(".git", "objects", subDir, fileName)
+ }
+}
+
+final case class Blob(content: Seq[Byte]) extends GitObject("blob") {
+ override def data: Array[Byte] = content.toArray[Byte]
+ lazy val asString: String = new String(data, UTF_8)
+}
+object Blob {
+ def apply(data: Array[Byte]): Blob = new Blob(data.toSeq)
+ def apply(str: String): Blob = new Blob(str.getBytes(UTF_8).toSeq)
+}
+
+final case class Tree(items: Seq[Tree.Item]) extends GitObject("tree") {
+ override def data: Array[Byte] = items.foldLeft(Array.empty[Byte]) { (acc, el) => acc ++ el.toBytes }
+}
+object Tree {
+ def apply(data: Array[Byte]): Tree = {
+ if (!data.contains(0.toByte)) throw new IllegalArgumentException("Not found any delimiter")
+
+ val items = mutable.Buffer.empty[Item]
+ var start = 0
+ while(start < data.length) {
+ val div: Int = data.indexOf(0.toByte, start)
+ val asString = new String(data.slice(start, div), UTF_8)
+ asString.split(" ").toList match {
+ case mode :: name => items.addOne(Item(Mode(mode), name.mkString(" "), new SHA1(data.slice(div + 1, div + 21).toSeq)))
+ case other => throw new IllegalArgumentException(s"Doesn't look like a tree item: `${other.mkString(";")}`")
+ }
+ start = div + 21
+ }
+
+ new Tree(items.toSeq)
+ }
+
+ enum Mode(val code: String):
+ case File extends Mode("100644")
+ case GroupFile extends Mode("100664")
+ case Executable extends Mode("100755")
+ case SymbolicLink extends Mode("120000")
+ case SubTree extends Mode("40000")
+ case SubModule extends Mode("160000")
+
+ object Mode {
+ def apply(code: String): Mode =
+ values.find(_.code == code).getOrElse {
+ throw new IllegalArgumentException(s"Not a mode code: $code")
+ }
+ }
+
+ case class Item(mode: Mode, name: String, hash: SHA1) {
+ def size: Int = name.length + 28 // `mode name\0sha-1` => 6 + 1 + name.size + 1 + 20
+ def toBytes: Array[Byte] = ((mode.code + " " + name).getBytes(UTF_8) :+ 0.toByte) ++ hash.hash
+ }
+}
+
+final case class Commit(
+ tree: SHA1,
+ parent: Seq[SHA1],
+ author: Commit.AuthRecord,
+ committer: Commit.AuthRecord,
+ message: String,
+ signature: Option[String] = None,
+ originalData: Option[Seq[Byte]] = None
+) extends GitObject("commit") {
+ override def data: Array[Byte] = {
+ originalData match
+ case None =>
+ val buff = new StringBuilder()
+ buff.append(s"tree $tree\n")
+ parent.foreach { p => buff.append(s"parent $p\n") }
+ buff.append(s"author $author\n")
+ buff.append(s"committer $committer\n")
+ signature.foreach { s => buff
+ .append("gpgsig -----BEGIN PGP SIGNATURE-----\n")
+ .append(s)
+ .append(" -----END PGP SIGNATURE-----")
+ }
+ buff.append(s"\n\n$message")
+ buff.toString().getBytes(UTF_8)
+
+ case Some(msg) => msg.toArray[Byte]
+ }
+}
+object Commit {
+ enum Field(val regex: Regex):
+ case Tree extends Field(raw"tree (\w{40})".r)
+ case Parent extends Field(raw"\nparent (\w{40})".r)
+ case Author extends Field(raw"\nauthor (.+) <(.+)> (\d{10,20}) (.+)".r)
+ case Committer extends Field(raw"\ncommitter (.+) <(.+)> (\d{10,20}) (.+)".r)
+ case Signature extends Field(raw"\ngpgsig -----BEGIN PGP SIGNATURE-----(.*\n)* -----END PGP SIGNATURE-----".r)
+ case Message extends Field(raw"(-----END PGP SIGNATURE-----)?\n\n(.*\n?)*".r)
+
+ def apply(data: Array[Byte]): Commit = {
+ val str = new String(data, UTF_8)
+ val asMap = Field.values.toSeq.map { f => f -> f.regex.findAllMatchIn(str) }.toMap
+ val signature = asMap(Field.Signature)
+ val candidate = new Commit(
+ SHA1.fromHex(asMap(Field.Tree).map(_.group(1)).toSeq.head),
+ asMap(Field.Parent).map(_.group(1)).toSeq.map(SHA1.fromHex),
+ asMap(Field.Author).map(AuthRecord.apply).toSeq.head,
+ asMap(Field.Committer).map(AuthRecord.apply).toSeq.head,
+ asMap(Field.Message).mkString("\n"),
+ if(signature.nonEmpty) Some(signature.mkString("\n")) else None
+ )
+
+ if(new String(candidate.data, UTF_8) == str) {
+ candidate
+ } else {
+ candidate.copy(originalData = Some(data.toSeq))
+ }
+ }
+
+ private val tsFormat: DateTimeFormatter = new DateTimeFormatterBuilder()
+ .appendValue(ChronoField.INSTANT_SECONDS, 1, 19, SignStyle.NEVER)
+ .appendValue(ChronoField.MILLI_OF_SECOND, 3)
+ .appendLiteral(" ")
+ .appendOffset("+HHMMss", "0")
+ .toFormatter();
+
+ case class AuthRecord(name: String, email: String, when: ZonedDateTime) {
+ override def toString: String = s"$name $email ${tsFormat.format(when)}"
+ }
+
+ object AuthRecord {
+ def apply(reg: Regex.Match): AuthRecord = new AuthRecord(
+ reg.group(1),
+ reg.group(2),
+ Instant.ofEpochMilli(reg.group(3).toLong)
+ .atZone(ZoneId.of(reg.group(4)))
+ )
+ }
+}
+
+final case class Tag(
+ link: SHA1,
+ tagType: String,
+ tag: String,
+ tagger: Option[Commit.AuthRecord],
+ message: String,
+ signature: Option[String] = None,
+ originalData: Option[Seq[Byte]] = None
+) extends GitObject("tag") {
+ override def data: Array[Byte] = {
+ originalData match
+ case None =>
+ val buff = new StringBuilder()
+ buff.append(s"object $link\n")
+ buff.append(s"type $tagType\n")
+ buff.append(s"tag $tag\n")
+ tagger.foreach { t => buff.append(s"tagger $t\n") }
+ signature.foreach { s =>
+ buff
+ .append("gpgsig -----BEGIN PGP SIGNATURE-----\n")
+ .append(s)
+ .append(" -----END PGP SIGNATURE-----")
+ }
+ buff.append(s"\n\n$message")
+ buff.toString().getBytes(UTF_8)
+
+ case Some(msg) => msg.toArray[Byte]
+ }
+}
+object Tag {
+ enum Field(val regex: Regex):
+ case Object extends Field(raw"object (\w{40})".r)
+ case TagType extends Field(raw"\ntype (\w+)".r)
+ case TagName extends Field(raw"\ntag (\w+)".r)
+ case Tagger extends Field(raw"\ntagger (.+) <(.+)> (\d{10,20}) (.+)".r)
+ case Signature extends Field(raw"\ngpgsig -----BEGIN PGP SIGNATURE-----(.*\n)* -----END PGP SIGNATURE-----".r)
+ case Message extends Field(raw"(-----END PGP SIGNATURE-----)?\n\n(.*\n?)*".r)
+
+ def apply(data: Array[Byte]): Tag = {
+ val str = new String(data, UTF_8)
+ val asMap = Field.values.toSeq.map { f => f -> f.regex.findAllMatchIn(str) }.toMap
+ val signature = asMap(Field.Signature)
+ val candidate = new Tag(
+ SHA1.fromHex(asMap(Field.Object).map(_.group(1)).toSeq.head),
+ asMap(Field.TagType).map(_.group(1)).toSeq.head,
+ asMap(Field.TagName).map(_.group(1)).toSeq.head,
+ asMap(Field.Tagger).map(Commit.AuthRecord.apply).toSeq.headOption,
+ asMap(Field.Message).mkString("\n"),
+ if(signature.nonEmpty) Some(signature.mkString("\n")) else None
+ )
+
+ if (new String(candidate.data, UTF_8) == str) {
+ candidate
+ } else {
+ candidate.copy(originalData = Some(data.toSeq))
+ }
+ }
+}
+
+case class RefDelta(
+ link: SHA1,
+ deltas: Seq[Deltified],
+ deflated: Boolean,
+ original: Seq[Byte] //TODO -> Seq[Delta],
+) extends GitObject("ref-delta") {
+ override def data: Array[Byte] = link.asArray ++ ZLib.pack(original.toArray[Byte])
+}
+object RefDelta {
+ def apply(data: Array[Byte]): RefDelta = {
+ val isDeflated = data.slice(20, 22).toSeq == Seq(120.toByte, -100.toByte)
+ val slice = data.slice(20, data.length)
+ val unpacked = if(isDeflated) ZLib.unpack(slice) else slice
+ new RefDelta(
+ new SHA1(data.take(20).toSeq),
+ Deltified.fromBytes(unpacked),
+ isDeflated,
+ unpacked.toSeq
+ )
+ }
+}
+
+enum Deltified {
+ case Copy(offset: Int, size: Int)
+ case Add(offset: Int, data: Seq[Byte])
+}
+
+object Deltified {
+ def fromBytes(data: Array[Byte]): Seq[Deltified] = {
+ val buff = ByteBuffer.wrap(data)
+ val deltas = mutable.Buffer.empty[Deltified]
+ while (buff.hasRemaining) {
+ deltas += ByteUtils.delta(buff)
+ }
+ deltas.toSeq
+ }
+}
diff --git a/git/src/main/scala/dev/rudiments/git/Pack.scala b/git/src/main/scala/dev/rudiments/git/Pack.scala
new file mode 100644
index 00000000..d7aca8db
--- /dev/null
+++ b/git/src/main/scala/dev/rudiments/git/Pack.scala
@@ -0,0 +1,107 @@
+package dev.rudiments.git
+
+import dev.rudiments.utils.{ CRC, SHA1 }
+
+import java.nio.ByteBuffer
+import java.nio.charset.StandardCharsets.UTF_8
+import java.nio.file.{ Files, Path }
+import scala.collection.immutable.ArraySeq
+
+case class Pack(objects: List[(SHA1, Pack.Entry)]) {
+ lazy val hashIndex: Map[SHA1, Pack.Entry] = objects.toMap
+ //need maps?
+}
+
+object Pack {
+ enum PackObj:
+ case Commit, Tree, Blob, Tag, ForFutureUse, OffsetDelta, RefDelta
+
+ object PackObj {
+ private val objMask = (7 << 4).toByte
+ def apply(b: Byte): PackObj = {
+ val code = (b & objMask) >> 4
+ PackObj.values(code - 1)
+ }
+ }
+
+ case class Entry(what: PackObj, size: Int, data: Array[Byte], from: Int, to: Int)
+
+ def readIdx(repo: Path, hash: String, size: Int): List[(SHA1, Int)] = {
+ val path = repo.resolve(Path.of(".git", "objects", "pack", s"pack-$hash.idx"))
+ val data = Files.readAllBytes(path)
+ val header = Array(255, 116, 79, 99, 0, 0, 0, 2).map(_.toByte)
+ assume(data.slice(0, 8).sameElements(header), "Expecting magic bytes and version 2")
+ //TODO use fanout when search in file?
+ val shaFrom = 8 + 256 * 4
+ val shaUntil = shaFrom + size * 20
+ val crcUntil = shaUntil + size * 4
+ val offsetsUntil = crcUntil + size * 4
+ val trailerUntil = offsetsUntil + 40
+ val sha = data.slice(shaFrom, shaUntil).grouped(20).map(h => new SHA1(ArraySeq.unsafeWrapArray(h))).toList
+ val crc = data.slice(shaUntil, crcUntil).grouped(4).map(c => new CRC(c)).toList
+ val offsets = data.slice(crcUntil, offsetsUntil).grouped(4).map(b => ByteBuffer.wrap(b).getInt).toList
+ val trailer = data.slice(offsetsUntil, offsetsUntil + 40) //TODO verify integrity
+
+ assume(data.length == trailerUntil)
+ assume(sha.size == crc.size, "List of hashes and list of CRC should be the same")
+ assume(sha.size == offsets.size, "List of hashes and list of offsets should be the same")
+ sha.zip(offsets).sortBy(_._2)
+ }
+
+ def readPack(repo: Path, hash: String): Pack = {
+ val path = repo.resolve(Path.of(".git", "objects", "pack", s"pack-$hash.pack"))
+
+ val bytes = Files.readAllBytes(path)
+ val count = readPackMeta(bytes)
+ val idx = readIdx(repo, hash, count)
+
+ if(idx.nonEmpty) {
+ val tail = new SHA1(Seq.empty) -> (bytes.length - 20)
+
+ val pack = (idx :+ tail).sliding(2).map {
+ case f :: s :: Nil => f._1 -> readEntry(bytes, f._2, s._2)
+ case _ => ???
+ }.toList
+
+ Pack(pack)
+ } else {
+ Pack(Nil)
+ }
+ }
+
+ private def readPackMeta(bytes: Array[Byte]): Int = {
+ val buf = ByteBuffer.allocate(4)
+
+ val packHeader = new String(bytes.take(4), UTF_8)
+ assume(packHeader == "PACK", "Not a pack")
+
+ val version = buf.put(0, bytes.slice(4, 8)).getInt
+ assume(version == 2, "Only version 2 is supported")
+
+ buf.clear()
+ buf.put(0, bytes.slice(8, 12)).getInt
+ }
+
+ private def readEntry(bytes: Array[Byte], from: Int, until: Int): Entry = { //TODO move to ByteUtils & rewrite with ByteBuffer
+ var address = from
+ var b = bytes(address)
+ val objType = PackObj(b)
+
+ var size = b & 0xF
+ var cursor = 4
+
+ while (nextIsSize(b) && cursor <= 32) {
+ address += 1
+ b = bytes(address)
+ val delta = (b & 0x7F) << cursor
+ size = size | delta
+ cursor += 7
+ }
+
+ val data = bytes.slice(address + 1, until)
+ //val data = ZLib.unpack(compressed)
+ Entry(objType, size, data, address + 1, until)
+ }
+
+ private def nextIsSize(b: Byte): Boolean = (b & 0x80).toByte == -128.toByte
+}
diff --git a/git/src/main/scala/dev/rudiments/git/Reader.scala b/git/src/main/scala/dev/rudiments/git/Reader.scala
new file mode 100644
index 00000000..cb0b4867
--- /dev/null
+++ b/git/src/main/scala/dev/rudiments/git/Reader.scala
@@ -0,0 +1,42 @@
+package dev.rudiments.git
+
+import dev.rudiments.utils.ZLib
+
+import java.lang
+import java.nio.charset.StandardCharsets.UTF_8
+import java.nio.file.{Files, Path}
+
+object Reader {
+ def read(repoDir: Path, hash: String): Either[Exception, GitObject] = {
+ val subDir = hash.take(2)
+ val fileName = hash.drop(2)
+ val path = repoDir.resolve(Path.of(".git", "objects", subDir, fileName)).normalize()
+
+ try {
+ val compressed = Files.readAllBytes(path)
+ val data = ZLib.unpack(compressed)
+ if (!data.contains(0.toByte)) {
+ Left(new IllegalArgumentException("Not found header-content delimiter"))
+ } else {
+ val headerIdx = data.indexOf(0.toByte)
+ val header = new String(data.take(headerIdx), UTF_8)
+ val content = data.drop(headerIdx + 1)
+ header.split(" ").toList match
+ case "blob" :: size :: Nil =>
+ val blob = Blob(content)
+ blob.validate(size.toInt, hash)
+ case "tree" :: size :: Nil =>
+ val tree = Tree(content)
+ tree.validate(size.toInt, hash)
+ case "commit" :: size :: Nil =>
+ val commit = Commit(content)
+ commit.validate(size.toInt, hash)
+ case "object" :: _ => Left(new IllegalArgumentException("Tags not supported yet"))
+ case other :: _ :: Nil => Left(new IllegalArgumentException(s"Not supported git object type: $other"))
+ case _ => Left(new IllegalArgumentException("Wrong format of a git object"))
+ }
+ } catch {
+ case e: Exception => Left(e)
+ }
+ }
+}
diff --git a/git/src/main/scala/dev/rudiments/git/Repository.scala b/git/src/main/scala/dev/rudiments/git/Repository.scala
new file mode 100644
index 00000000..f3907038
--- /dev/null
+++ b/git/src/main/scala/dev/rudiments/git/Repository.scala
@@ -0,0 +1,119 @@
+package dev.rudiments.git
+
+import dev.rudiments.git.Pack.{ Entry, PackObj }
+import dev.rudiments.utils.{ Log, SHA1, ZLib }
+
+import java.nio.file.{ Files, Path }
+import scala.collection.mutable
+
+class Repository(root: Path) extends Log {
+ var head: Commit = _
+
+ val branches: mutable.Buffer[String] = mutable.Buffer.empty
+ val heads: mutable.Buffer[String] = mutable.Buffer.empty
+ val tags: mutable.Buffer[String] = mutable.Buffer.empty
+ val objects: mutable.Map[SHA1, GitObject] = mutable.HashMap.empty
+
+ val usedIn: mutable.Map[SHA1, mutable.Set[SHA1]] = mutable.Map.empty
+
+ val errors: mutable.Map[SHA1, (Entry, String)] = mutable.HashMap.empty
+
+ private val packPath = root.resolve(Path.of(".git", "objects", "pack"))
+
+ def read(): Unit = {
+ val packPattern = "pack-(\\w+).pack".r
+
+ Files.list(packPath).filter { f =>
+ val s = f.getFileName.toString
+ s.startsWith("pack") && s.endsWith(".pack")
+ }.forEach { p =>
+ packPattern.findFirstMatchIn(p.getFileName.toString).map(_.group(1)).foreach { pack =>
+ log.info("Reading pack {}", pack)
+ readPack(pack)
+ }
+ }
+ }
+
+ def readPack(hash: String): Unit = {
+ val packEntries = Pack.readPack(root, hash).objects
+ val initialObjects = objects.size
+ val initialErros = errors.size
+ packEntries.foreach {
+ case (key, v@Entry(PackObj.Tree, size, data, _, _)) =>
+ try {
+ Tree(ZLib.unpack(data))
+ .validate(size, key.string) match
+ case Right(r) => objects.put(key, r)
+ case Left(e) => errors.put(key, (v, e.getLocalizedMessage))
+ } catch {
+ case e: Exception => errors.put(key, (v, e.getLocalizedMessage))
+ }
+ case (key, v@Entry(PackObj.Commit, size, data, _, _)) =>
+ try {
+ Commit(ZLib.unpack(data))
+ .validate(size, key.string) match
+ case Right(r) => objects.put(key, r)
+ case Left(e) => errors.put(key, (v, e.getLocalizedMessage))
+ } catch {
+ case e: Exception => errors.put(key, (v, e.getLocalizedMessage))
+ }
+
+ case (key, v@Entry(PackObj.Blob, size, data, _, _)) =>
+ try {
+ Blob(ZLib.unpack(data))
+ .validate(size, key.string) match
+ case Right(r) => objects.put(key, r)
+ case Left(e) => errors.put(key, (v, e.getLocalizedMessage))
+ } catch {
+ case e: Exception => errors.put(key, (v, e.getLocalizedMessage))
+ }
+
+ case (key, v@Entry(PackObj.Tag, size, data, _, _)) =>
+ try {
+ Tag(ZLib.unpack(data))
+ .validate(size, key.string) match
+ case Right(r) => objects.put(key, r)
+ case Left(e) => errors.put(key, (v, e.getLocalizedMessage))
+ } catch {
+ case e: Exception => errors.put(key, (v, e.getLocalizedMessage))
+ }
+
+ case (key, v@Entry(PackObj.RefDelta, _, data, _, _)) =>
+ try {
+ val delta = RefDelta(data)
+ objects.get(delta.link) match
+ case Some(_) => objects.put(key, delta)
+ case None => errors.put(key, (v, "reference not exist"))
+ } catch {
+ case e: Exception => errors.put(key, (v, e.getLocalizedMessage))
+ }
+
+ case (key, entry) => errors.put(key, (entry, "Parse are not implemented"))
+ }
+
+ //resolving deltas
+ val toRemove = errors.collect {
+ case (k, (Pack.Entry(PackObj.RefDelta, _, d, _, _), _)) =>
+ try {
+ val delta = RefDelta(d)
+ objects.get(delta.link).map { _ =>
+ objects.put(k, delta)
+ k
+ }
+ } catch {
+ case e: Exception => None
+ }
+ }.flatten
+ errors --= toRemove
+
+ //TODO index usedIn /objects.foreach { (k, v) => }
+
+ if(errors.size - initialErros > 0) {
+ log.error(s"Can't parse {${errors.size - initialErros}} entries into objects")
+ val groupped: Map[PackObj, Iterable[(Entry, String)]] = errors.values.groupBy(_._1.what)
+ val counted = groupped.map { (k, v) => k -> v.size }.toSeq.sortBy(_._2)
+ log.error("Errors by groups: {}", counted.mkString(";"))
+ }
+ log.info(s"Parsed ${packEntries.size} entries into ${objects.size - initialObjects} objects")
+ }
+}
diff --git a/git/src/main/scala/dev/rudiments/git/Writer.scala b/git/src/main/scala/dev/rudiments/git/Writer.scala
new file mode 100644
index 00000000..fd0968d4
--- /dev/null
+++ b/git/src/main/scala/dev/rudiments/git/Writer.scala
@@ -0,0 +1,33 @@
+package dev.rudiments.git
+
+import dev.rudiments.utils.ZLib
+
+import java.nio.file.{ Files, Path }
+
+object Writer {
+ def write(repoDir: Path, obj: GitObject): Status = {
+ val path = repoDir.resolve(obj.objectPath).normalize()
+ import java.nio.file.StandardOpenOption.*
+ try {
+ val compressed = ZLib.pack(obj.full)
+ Files.write(path, compressed, CREATE_NEW, WRITE)
+ Status.Success
+ } catch {
+ case e: Exception => Status.Failure(e)
+ }
+ }
+
+ def deleteIfExist(repoDir: Path, obj: GitObject): Status = {
+ val path = repoDir.resolve(obj.objectPath).normalize()
+ try {
+ Files.deleteIfExists(path)
+ Status.Success
+ } catch {
+ case e: Exception => Status.Failure(e)
+ }
+ }
+
+ enum Status:
+ case Success
+ case Failure(e: Exception)
+}
diff --git a/git/src/test/scala/test/dev/rudiments/git/GitBlobTest.scala b/git/src/test/scala/test/dev/rudiments/git/GitBlobTest.scala
new file mode 100644
index 00000000..fc8be92c
--- /dev/null
+++ b/git/src/test/scala/test/dev/rudiments/git/GitBlobTest.scala
@@ -0,0 +1,27 @@
+package test.dev.rudiments.git
+
+import dev.rudiments.git.Blob
+import dev.rudiments.utils.SHA1
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+
+class GitBlobTest extends AnyWordSpec with Matchers {
+ "Git BLOB" should {
+ "fit header with example" in {
+ // $echo -n "what is up, doc?" | git hash-object --stdin
+ val blob = Blob("what is up, doc?")
+ blob.header should be ("blob 16")
+ blob.hash.toString should be("bd9dbf5aae1a3862dd1526723246b20206e5fc37")
+ }
+
+ "fit with known hashes" in {
+ val known = Map(
+ "git compatible" -> "51e7ed8563dcc08a564795ead8899a8ced95838c",
+ "sha-1" -> "ea9090c10ac8e06b8d50114e6816042d5a7e16d8"
+ )
+
+ val hashed = known.map((k, _) => k -> Blob(k).hash.toString)
+ hashed should be(known)
+ }
+ }
+}
diff --git a/git/src/test/scala/test/dev/rudiments/git/GitCommitsTest.scala b/git/src/test/scala/test/dev/rudiments/git/GitCommitsTest.scala
new file mode 100644
index 00000000..99a8dea4
--- /dev/null
+++ b/git/src/test/scala/test/dev/rudiments/git/GitCommitsTest.scala
@@ -0,0 +1,30 @@
+package test.dev.rudiments.git
+
+import dev.rudiments.git.{Commit, Reader}
+import dev.rudiments.utils.Log
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+
+import java.nio.file.{Files, Path}
+
+class GitCommitsTest extends AnyWordSpec with Matchers with Log {
+ private val dir = Path.of("..").toAbsolutePath //TODO fix
+
+ "can read chain of commits till the first one" ignore {
+ var h = "a3e9375e5ea70baf7d6a4ba343c59619aee1f2f0"
+ var i = 0; // up to 37
+
+ while(h != "SUCCESS" || h != "FAIL") {
+ Reader.read(dir, h) match {
+ case Right(c: Commit) if c.parent.nonEmpty =>
+ h = c.parent.head.toString
+ i += 1
+ case Right(_) => h = "SUCCESS"
+ case Left(err) => h = "FAIL"
+ throw err
+ }
+ }
+
+ //5d631a5fb3318f3cf14ba3c7e0aba9e6674b8944
+ }
+}
diff --git a/git/src/test/scala/test/dev/rudiments/git/GitObjectTest.scala b/git/src/test/scala/test/dev/rudiments/git/GitObjectTest.scala
new file mode 100644
index 00000000..24e71a81
--- /dev/null
+++ b/git/src/test/scala/test/dev/rudiments/git/GitObjectTest.scala
@@ -0,0 +1,71 @@
+package test.dev.rudiments.git
+
+import dev.rudiments.git.{Blob, Commit, Reader, Tree, Writer}
+import dev.rudiments.utils.Log
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+
+import java.nio.file.{Files, Path}
+
+class GitObjectTest extends AnyWordSpec with Matchers with Log {
+ private val dir = Path.of("..").toAbsolutePath //TODO fix
+
+ "git objects" ignore { //TODO read this objects from the repository
+ "can read blob from repo" in {
+ val readen = Reader.read(dir, "ce428f3bf04949f1386c964c5ef6c282861ec64a")
+ readen match
+ case Left(err) => throw err
+ case Right(obj) => obj should be(Blob("dependencies {\n implementation project(':core')\n implementation project(':file')\n}\n"))
+ }
+
+ "can read tree from repo" in {
+ val readen = Reader.read(dir, "a530c65ac6f8fc4323004876a159726f84c278b9")
+ readen match
+ case Left(err) => throw err
+ case Right(obj) =>
+ obj.header should be("tree 620")
+ }
+
+ "can write blob into repo" in {
+ val someStuff = Blob("some stuff\n")
+ Writer.deleteIfExist(dir, someStuff) should be(Writer.Status.Success)
+ Writer.write(dir, someStuff) should be(Writer.Status.Success)
+ Reader.read(dir, "b5fd817de972cdb092b7dfbeeb1bedb4f05eb218") should be(Right(someStuff))
+ }
+
+ "can write tree into repo" in {
+ val someBlob = Blob("some test blob\n")
+ Writer.deleteIfExist(dir, someBlob) should be(Writer.Status.Success)
+ Writer.write(dir, someBlob) should be(Writer.Status.Success)
+
+ val someTree = Tree(Seq(Tree.Item(Tree.Mode.File, "some_blob", someBlob.hash)))
+ Writer.deleteIfExist(dir, someTree)
+ Writer.write(dir, someTree)
+ Reader.read(dir, "cd7641db93deb932638f99daa916b0e1d2d93e51") should be(Right(someTree))
+ }
+
+ "can read commit from repo" in {
+ val readen = Reader.read(dir, "a3e9375e5ea70baf7d6a4ba343c59619aee1f2f0")
+ readen match
+ case Left(err) => throw err
+ case Right(c) =>
+ c.header should be("commit 238")
+ }
+
+ "can read parent and tree of commit" in {
+ val result = for {
+ first <- Reader.read(dir, "a3e9375e5ea70baf7d6a4ba343c59619aee1f2f0")
+ tree <- Reader.read(dir, first.asInstanceOf[Commit].tree.toString)
+ second <- Reader.read(dir, first.asInstanceOf[Commit].parent.head.toString)
+ } yield (first, tree, second)
+
+ result match {
+ case Left(err) => throw err
+ case Right(f, t, s) =>
+ f.header should be("commit 238")
+ t.header should be("tree 620")
+ s.header should be("commit 275")
+ }
+ }
+ }
+}
diff --git a/git/src/test/scala/test/dev/rudiments/git/PackTest.scala b/git/src/test/scala/test/dev/rudiments/git/PackTest.scala
new file mode 100644
index 00000000..414185f6
--- /dev/null
+++ b/git/src/test/scala/test/dev/rudiments/git/PackTest.scala
@@ -0,0 +1,56 @@
+package test.dev.rudiments.git
+
+import dev.rudiments.git.{ByteUtils, Deltified, Pack, RefDelta}
+import dev.rudiments.git.Pack.PackObj
+import dev.rudiments.utils.{Hashed, Log}
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+
+import java.nio.ByteBuffer
+import java.nio.file.{Files, Path}
+
+class PackTest extends AnyWordSpec with Matchers with Log {
+ private val dir = Path.of("..").toAbsolutePath //TODO fix
+
+ "can read pack index" ignore { //TODO use actual pack file, otherwise it checked while reading repository
+ val hash = "8cc0a2aa174783656e7d32edb2993b578c957c2d"
+ val readen = Pack.readPack(dir, hash)
+ readen.objects.size should be (367)
+ }
+
+ "can parse ref delta from byte array" ignore {
+ val hex = "e5614b53 b3699d9fc08d41135b16a4a875b0fc68 789c 6b38cad8718071034702 00 142c03a8".replace(" ", "")
+ val data = Hashed.hexFormat.parseHex(hex)
+ val delta = RefDelta(data)
+
+ delta.original should be (Hashed.hexFormat.parseHex("80c50188c001b00860").toSeq)
+
+ val buff = ByteBuffer.wrap(delta.original.toArray[Byte])
+ buff.position() should be (0)
+ buff.get() should be (0x80.toByte)
+ ByteUtils.variableSize(buff) should be (197) // offset?
+ buff.position() should be (3)
+ ByteUtils.variableSize(buff) should be (24584) // result size!
+ buff.position() should be (6)
+ ByteUtils.variableSize(buff) should be (1072)
+ buff.position() should be (8)
+ buff.get() should be (0x60.toByte)
+ }
+
+ // e5614b53 b3699d9fc08d41135b16a4a875b0fc68 789c 6b38cad8718071034702 00 142c03a8
+ // e5614b53 b3699d9fc08d41135b16a4a875b0fc68 -> f64062be 71facf76fba819b036562a513e6ba1b1
+ // 789c 6b38cad8718071034702 00 142c03a8
+ // 80 c501 88c001 b008 60
+ // -128 -59 1 -120 -64 1 -80 8 96
+/*
+ 10000000
+ 11000101
+ 00000001
+ 10001000
+ 11000000
+ 00000001
+ 10110000
+ 00001000
+ 01100000
+*/
+}
diff --git a/git/src/test/scala/test/dev/rudiments/git/RepositoryTest.scala b/git/src/test/scala/test/dev/rudiments/git/RepositoryTest.scala
new file mode 100644
index 00000000..d530e8e2
--- /dev/null
+++ b/git/src/test/scala/test/dev/rudiments/git/RepositoryTest.scala
@@ -0,0 +1,19 @@
+package test.dev.rudiments.git
+
+import dev.rudiments.git.{Pack, Repository}
+import dev.rudiments.git.Pack.PackObj
+import dev.rudiments.utils.Log
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+
+import java.nio.file.{Files, Path}
+
+class RepositoryTest extends AnyWordSpec with Matchers with Log {
+ private val dir = Path.of("..").toAbsolutePath //TODO fix
+
+ private val repo = new Repository(dir)
+
+ "can read packs" in {
+ repo.read() //TODO fails on initial commit
+ }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 00000000..4ac3234a
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,2 @@
+# This file was generated by the Gradle 'init' task.
+# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 7454180f..d64cd491 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 2e6e5897..1af9e093 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 1b6c7873..aeb74cbb 100755
--- a/gradlew
+++ b/gradlew
@@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,13 +80,10 @@ do
esac
done
-APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
-
-APP_NAME="Gradle"
+# This is normally unused
+# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -143,12 +140,16 @@ fi
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -193,6 +194,10 @@ if "$cygwin" || "$msys" ; then
done
fi
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
@@ -205,6 +210,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \
"$@"
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
diff --git a/gradlew.bat b/gradlew.bat
index ac1b06f9..6689b85b 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
-@if "%DEBUG%" == "" @echo off
+@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +25,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto execute
+if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
+if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
diff --git a/http/build.gradle b/http/build.gradle
deleted file mode 100644
index dbe4c3a7..00000000
--- a/http/build.gradle
+++ /dev/null
@@ -1,13 +0,0 @@
-dependencies {
- implementation project(':core')
-
- implementation 'com.typesafe.akka:akka-http_2.13:10.2.7'
- implementation 'ch.megard:akka-http-cors_2.13:1.1.2'
- implementation 'io.circe:circe-core_2.13:0.14.1'
- implementation 'io.circe:circe-generic_2.13:0.14.1'
- implementation 'io.circe:circe-generic-extras_2.13:0.14.1'
- implementation 'de.heikoseeberger:akka-http-circe_2.13:1.38.2'
-
- testImplementation 'com.typesafe.akka:akka-testkit_2.13:2.6.17'
- testImplementation 'com.typesafe.akka:akka-http-testkit_2.13:10.2.7'
-}
\ No newline at end of file
diff --git a/http/src/main/scala/dev/rudiments/hardcore/http/CirceSupport.scala b/http/src/main/scala/dev/rudiments/hardcore/http/CirceSupport.scala
deleted file mode 100644
index ed16471b..00000000
--- a/http/src/main/scala/dev/rudiments/hardcore/http/CirceSupport.scala
+++ /dev/null
@@ -1,14 +0,0 @@
-package dev.rudiments.hardcore.http
-
-import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport
-import dev.rudiments.hardcore._
-import io.circe._
-import io.circe.generic.extras.Configuration
-
-trait CirceSupport extends FailFastCirceSupport {
- implicit val configuration: Configuration = Configuration.default.withDefaults
- implicit val printer: Printer = Printer.noSpaces.copy(dropNullValues = true)
-
- implicit val thingEncoder: Encoder[Thing] = ThingEncoder.encodeAnything
- implicit val dataEncoder: Encoder[Data] = ThingEncoder.encodeData
-}
diff --git a/http/src/main/scala/dev/rudiments/hardcore/http/RootRouter.scala b/http/src/main/scala/dev/rudiments/hardcore/http/RootRouter.scala
deleted file mode 100644
index 16688ac1..00000000
--- a/http/src/main/scala/dev/rudiments/hardcore/http/RootRouter.scala
+++ /dev/null
@@ -1,78 +0,0 @@
-package dev.rudiments.hardcore.http
-
-import akka.Done
-import akka.actor.ActorSystem
-import akka.http.scaladsl.Http
-import akka.http.scaladsl.server.{Directives, Route}
-import akka.http.scaladsl.server.Directives._
-import ch.megard.akka.http.cors.scaladsl.CorsDirectives
-import ch.megard.akka.http.cors.scaladsl.settings.CorsSettings
-import com.typesafe.scalalogging.StrictLogging
-import dev.rudiments.hardcore._
-import dev.rudiments.hardcore.http.RootRouter.RootConfig
-
-import java.lang.management.ManagementFactory
-import scala.concurrent.ExecutionContext
-import scala.util.{Failure, Success}
-
-class RootRouter(
- config: RootConfig,
- routes: (String, Route)*
-)(implicit actorSystem: ActorSystem) extends StrictLogging {
- private implicit val ec: ExecutionContext = actorSystem.getDispatcher
- val routers = new Memory()
-
- def route: Route = CorsDirectives.cors(config.cors) {
- if (config.prefix != "") {
- Directives.pathPrefix(config.prefix) {
- routes.map {
- case (p, router: Route) => Directives.pathPrefix(p) {
- router
- }
- }.reduce(_ ~ _)
- }
- } else {
- routes.map {
- case (p, router: Route) => Directives.pathPrefix(p) {
- router
- }
- }.reduce(_ ~ _)
- }
- }
-
-
- def bind(): Done = {
- Http().newServerAt(
- config.host,
- config.port
- ).bind(route).onComplete {
- case Success(b) => logger.info("Bound http:/{}/{} on {}", b.localAddress.toString, config.prefix, ManagementFactory.getRuntimeMXBean.getName)
- case Failure(e) =>
- actorSystem.terminate()
- throw e
- }
- Done
- }
-}
-
-object RootRouter {
- val rootPath = "http"
- val prefixPath = s"$rootPath.prefix"
- val hostPath = s"$rootPath.host"
- val portPath = s"$rootPath.port"
-
- case class RootConfig(
- host: String,
- port: Int,
- prefix: String = "",
- cors: CorsSettings
- )
-
- import com.typesafe.config.Config
- def config(c: Config): RootConfig = RootConfig(
- c.getString(hostPath),
- c.getInt(portPath),
- if(c.hasPath(prefixPath)) c.getString(prefixPath) else "",
- CorsSettings(c.getConfig(rootPath))
- )
-}
diff --git a/http/src/main/scala/dev/rudiments/hardcore/http/ScalaRouter.scala b/http/src/main/scala/dev/rudiments/hardcore/http/ScalaRouter.scala
deleted file mode 100644
index 4c1c9d68..00000000
--- a/http/src/main/scala/dev/rudiments/hardcore/http/ScalaRouter.scala
+++ /dev/null
@@ -1,94 +0,0 @@
-package dev.rudiments.hardcore.http
-
-import akka.http.scaladsl.model.StatusCodes
-import akka.http.scaladsl.server.Directives._
-import akka.http.scaladsl.server.{Route, StandardRoute}
-import dev.rudiments.hardcore._
-import io.circe.Decoder
-
-import scala.language.implicitConversions
-
-class ScalaRouter(mem: Node)(implicit td: ThingDecoder) extends CirceSupport {
- val routes: Route = {
- path(Segments(1, 128) ~ Slash) { segments =>
- get {
- mem.navigate(segments) match {
- case (_, Unmatched) => NotExist
- case (_, l) => mem ?? l
- }
- }
- } ~ path(Segments(1, 128)) { segments =>
- mem.navigate(segments) match {
- case (_, Unmatched) => NotExist
- case (node, location) =>
- implicit val d: Decoder[Data] = td.decoder(node.leafIs).map(_.asInstanceOf[Data])
- parameterMultiMap { params =>
- get {
- if (params.contains("structure")) {
- mem ?* location
- } else {
- mem ? location
- }
- } ~ delete {
- mem -= location
- } ~ entity(as[Data]) { data =>
- post {
- mem += location -> data
- } ~ put {
- mem *= location -> data
- }
- }
- }
- }
- } ~ pathEnd {
- parameterMultiMap { params =>
- get {
- if (params.contains("structure")) {
- mem ?* Root
- } else {
- mem ? Root
- }
- }
- }
- } ~ pathSingleSlash {
- get {
- mem ?? Root
- }
- }
- }
-
- private implicit def responseWith(event: Out): StandardRoute = event match {
- case Created(value) =>
- complete(StatusCodes.Created, value)
- case Readen(value) =>
- complete(StatusCodes.OK, value)
- case Updated(_, newValue) =>
- complete(StatusCodes.OK, newValue)
- case Deleted(_) =>
- complete(StatusCodes.NoContent)
- case Found(_, values) =>
- val node = Node.fromMap(values)
- complete(StatusCodes.OK, node.asInstanceOf[Thing])
- case NotExist =>
- complete(StatusCodes.NotFound)
- case _: NotFound =>
- complete(StatusCodes.NotFound)
- case AlreadyExist(_) =>
- complete(StatusCodes.Conflict)
- case out: CRUD.O =>
- complete(StatusCodes.OK, out.asInstanceOf[Thing])
-
- case _: Error =>
- complete(StatusCodes.InternalServerError)
- case _ =>
- complete(StatusCodes.InternalServerError)
- }
-
- def seal(): Route = this.seal("")
-
- def seal(prefix: String): Route = if(prefix != ""){
- Route.seal(pathPrefix(prefix) { routes })
- } else {
- Route.seal(routes)
- }
-}
diff --git a/http/src/main/scala/dev/rudiments/hardcore/http/ScalaTypes.scala b/http/src/main/scala/dev/rudiments/hardcore/http/ScalaTypes.scala
deleted file mode 100644
index 2ace722d..00000000
--- a/http/src/main/scala/dev/rudiments/hardcore/http/ScalaTypes.scala
+++ /dev/null
@@ -1,21 +0,0 @@
-package dev.rudiments.hardcore.http
-
-import dev.rudiments.hardcore._
-
-object ScalaTypes {
- val numbers: Map[ID, Predicate] = Map(
- ID("Byte") -> Number(Byte.MinValue, Byte.MaxValue),
- ID("Short") -> Number(Short.MinValue, Short.MaxValue),
- ID("Char") -> Number(Char.MinValue, Char.MaxValue),
- ID("Int") -> Number(Int.MinValue, Int.MaxValue),
- ID("Long") -> Number(Long.MinValue, Long.MaxValue),
- ID("Float") -> Number(Float.MinValue, Float.MaxValue),
- ID("Double") -> Number(Double.MinValue, Double.MaxValue),
- ID("BigInteger") -> Number(Anything, Double.MaxValue),
- )
-
- val text: Map[ID, Predicate] = Map(
- ID("String") -> Text(Int.MaxValue),
- ID("DefaultText") -> Text(1024)
- )
-}
diff --git a/http/src/main/scala/dev/rudiments/hardcore/http/ThingDecoder.scala b/http/src/main/scala/dev/rudiments/hardcore/http/ThingDecoder.scala
deleted file mode 100644
index dd7f0a24..00000000
--- a/http/src/main/scala/dev/rudiments/hardcore/http/ThingDecoder.scala
+++ /dev/null
@@ -1,284 +0,0 @@
-package dev.rudiments.hardcore.http
-
-import dev.rudiments.hardcore._
-import dev.rudiments.hardcore.http.ThingEncoder.discriminator
-import io.circe.{Decoder, DecodingFailure, HCursor, KeyDecoder}
-
-import java.sql
-import scala.collection.{Factory, mutable}
-
-class ThingDecoder(ts: TypeSystem) {
- def anythingDecoder: Decoder[Thing] = { c: HCursor =>
- c.downField(discriminator).as[String].flatMap { s =>
- val id = ID(s)
- val d = if(ts.typeSystem.contains(id)) {
- decoders(id)
- } else {
- alwaysFail(s"Not in /types: $s")
- }
- d.apply(c)
- }
- }
-
- val locationDecoders: Map[ID, Decoder[Thing]] = Map(
- ID("ID") -> Decoder { c: HCursor =>
- c.downField("key").as[String].map(s => ID(s).asInstanceOf[Thing])
- },
- ID("Path") -> Decoder { c: HCursor =>
- c.downField("ids").as[String]
- .map(s => Path.apply(s.split("/").map(ID): _*).asInstanceOf[Thing])
- },
- ID("Root") -> staticDecoder(Root),
- ID("Unmatched") -> staticDecoder(Unmatched),
- )
-
- val plainDecoders: Map[ID, Decoder[Predicate]] = Map(
- ID("Number") -> Decoder { _ => Right(Number(Long.MinValue, Long.MaxValue)) },
- ID("Text") -> Decoder { _ => Right(Text(1024)) },
- ID("Bool") -> Decoder { _ => Right(Bool) },
- ID("Binary") -> Decoder { _ => Right(Binary) },
-
- ID("Date") -> Decoder { _ => Right(Date) },
- ID("Time") -> Decoder { _ => Right(Time) },
- ID("Timestamp") -> Decoder { _ => Right(Timestamp) },
- )
-
- def predicateDecoder: Decoder[Predicate] = Decoder { c: HCursor =>
- c.downField(discriminator).as[String].flatMap { s =>
- val id = ID(s)
- if(plainDecoders.contains(id)) {
- plainDecoders(id).apply(c)
- } else {
- if (ts.predicates.contains(id)) {
- id match {
- case ID("Type") => typeDecoder.map(_.asInstanceOf[Predicate]).apply(c)
- case ID("Enlist") => c.downField("of").as(predicateDecoder.map(p => Enlist(p)))
- case ID("Index") => for {
- of <- c.downField("of").as(predicateDecoder)
- over <- c.downField("over").as(predicateDecoder)
- } yield Index(of, over)
- case ID("AnyOf") => c.downField("p")
- .as(Decoder.decodeArray(predicateDecoder, Factory.arrayFactory))
- .map(arr => AnyOf(arr:_*))
- case ID("Link") => Left(DecodingFailure(s"Not supported $id", List.empty))
- case ID("Declared") => Left(DecodingFailure(s"Not supported $id", List.empty))
- case _ => Left(DecodingFailure(s"Not implemented Predicate: $id", List.empty))
- }
- } else {
- ts.typeSystem.get(id) match {
- case Some(p) => Right(Link(ID("types") / id, p))
- case None =>
- Left(DecodingFailure(s"Not a Predicate: $id", List.empty))
- }
- }
- }
- }
- }
-
- def nodeDecoder: Decoder[Node] = Decoder { c =>
- for {
- self <- c.getOrElse("self")(Nothing.asInstanceOf[Thing])(anythingDecoder)
- keyIs <- c.getOrElse("key-is")(Nothing.asInstanceOf[Predicate])(predicateDecoder)
- leafIs <- c.getOrElse("leaf-is")(Nothing.asInstanceOf[Predicate])(predicateDecoder)
- leafs <- c.getOrElse("leafs")(Map.empty[ID, Thing]) {
- val d = leafIs match {
- case Link(id: ID, _) if ts.typeSystem.contains(id) => decoders(id)
- case Link(p: Path, _) if ts.typeSystem.contains(p.last) => decoders(p.last)
- case other => anythingDecoder
- }
- Decoder.decodeMap(idKeyDecoder, d)
- } //TODO propagate leafIs and keyIs predicates for decoding leafs
- branches <- c.getOrElse("branches")(Map.empty[ID, Node])(
- Decoder.decodeMap(idKeyDecoder, nodeDecoder)
- )
- relations <- c.getOrElse("relations")(Map.empty[Location, Seq[Location]])(
- Decoder.decodeMap(
- locKeyDecoder,
- Decoder
- .decodeArray(locDecoder, Factory.arrayFactory)
- .map(_.toSeq)))
- } yield new Node(
- self = self,
- leafs = mutable.Map.from(leafs),
- branches = mutable.Map.from(branches),
- relations = mutable.Map.from(relations),
- keyIs = keyIs,
- leafIs = leafIs
- )
- }
-
- private def typeDecoder: Decoder[Type] = { c: HCursor =>
- c.keys.getOrElse(throw new IllegalStateException("How this happened?"))
- .collect { case k if k != discriminator => c.downField(k).as(predicateDecoder).map(p => Field(k, p)) }
- .foldRight(Right(scala.Nil): Decoder.Result[scala.List[Field]]) { (e, acc) =>
- for (xs <- acc.right; x <- e.right) yield x :: xs
- }.map { l => Type(l: _*) }
- }
-
- val compositeDecoders: Map[ID, Decoder[Thing]] = Map(
- ID("Field") -> alwaysFail("Direct Field decoding not supported"),
- ID("Type") -> typeDecoder.map(_.asInstanceOf[Thing]),
- ID("Enlist") -> Decoder { c: HCursor => c.downField("of").as(predicateDecoder.map(p => Enlist(p).asInstanceOf[Thing])) },
- ID("Index") -> Decoder { c: HCursor =>
- for {
- of <- c.downField("of").as(predicateDecoder)
- over <- c.downField("over").as(predicateDecoder)
- } yield Index(of, over).asInstanceOf[Thing]
- },
- ID("AnyOf") -> Decoder { c: HCursor =>
- c.downField("p")
- .as(Decoder.decodeArray(predicateDecoder, Factory.arrayFactory))
- .map(arr => AnyOf(arr: _*))
- },
- ID("Link") -> alwaysFail("TODO Link"),
- ID("Declared") -> alwaysFail("TODO Declared"),
- )
-
- def commitDecoder: Decoder[Commit] = Decoder {_ => Left(DecodingFailure("TODO: Commit", List.empty)) }
-
- val messageDecoders: Map[ID, Decoder[Thing]] = Map(
- ID("Create") -> anythingDecoder.map(Create),
- ID("Read") -> staticDecoder(Read),
- ID("Update") -> anythingDecoder.map(Update),
- ID("Delete") -> staticDecoder(Delete),
- ID("Find") -> predicateDecoder.map(Find),
- ID("LookFor") -> predicateDecoder.map(LookFor),
- ID("Dump") -> predicateDecoder.map(Dump),
- ID("Prepare") -> staticDecoder(Prepare),
- ID("Verify") -> staticDecoder(Verify),
- ID("Commit") -> commitDecoder.map(_.asInstanceOf[Thing]),
-
- ID("Created") -> anythingDecoder.map(Created),
- ID("Readen") -> anythingDecoder.map(Readen),
- ID("Updated") -> Decoder { c: HCursor =>
- for {
- old <- c.downField("old").as(anythingDecoder)
- what <- c.downField("what").as(anythingDecoder)
- } yield Updated(old, what)
- },
- ID("Deleted") -> anythingDecoder.map(Deleted),
- ID("Found") -> Decoder { c: HCursor =>
- for {
- query <- c.downField("query").as(anythingDecoder).flatMap {
- case q: Query => Right(q)
- case other => Left(DecodingFailure("Not supported thing instead of Query", List.empty))
- }
- values <- c.downField("what").as(Decoder.decodeMap(locKeyDecoder, anythingDecoder))
- } yield Found(query, values)
- },
-
- ID("NotExist") -> staticDecoder(NotExist),
- ID("NotFound") -> Decoder { _.downField("missing").as(locDecoder).map(NotFound) },
- ID("Prepared") -> commitDecoder.map(cmt => Prepared(cmt).asInstanceOf[Thing]),
- ID("Identical") -> staticDecoder(Identical),
- ID("Valid") -> staticDecoder(Valid),
- ID("Committed") -> commitDecoder.map(cmt => Committed(cmt).asInstanceOf[Thing]),
-
- ID("AlreadyExist") -> anythingDecoder.map(AlreadyExist),
- ID("Conflict") -> alwaysFail("TODO: Conflict"),
- ID("MultiError") -> alwaysFail("TODO: MultiError"),
- ID("NotImplemented") -> staticDecoder(NotImplemented),
- ID("NotSupported") -> staticDecoder(NotSupported),
- )
-
- val linkDecoders: Map[ID, Decoder[Thing]] = ts.typeSystem.collect { // Location, Temporal, Plain, Predicate, Agent
- case (id: ID, l@Link(_, _: AnyOf)) => id -> staticDecoder(l) // Message, In, Out, Query, Command, Report, Event, Error, CRUD
- } // total: 14
-
- val decoders: Map[ID, Decoder[Thing]] = {
- val provided = locationDecoders ++ compositeDecoders ++ messageDecoders ++ linkDecoders ++
- Map(
- ID("Anything") -> staticDecoder(Anything),
- ID("Nothing") -> staticDecoder(Nothing),
- ID("Data") -> alwaysFail("TODO Data"),
- ID("Node") -> nodeDecoder.map(_.asInstanceOf[Thing]),
- )
-
- val rawTypes = ts.typeSystem.collect {
- case (id: ID, t: Type) => id -> dataTypeDecoder(t).map(dt => Data(Link(ID("types") / id, t), dt.data).asInstanceOf[Thing])
- case (p: Path, t: Type) => p.last -> dataTypeDecoder(t).map(dt => Data(Link(p, t), dt.data).asInstanceOf[Thing])
- }
- provided ++ (rawTypes -- provided.keys)
- }
-
- private def staticDecoder(what: Thing): Decoder[Thing] = Decoder { _: HCursor => Right(what) }
- private def alwaysFail(msg: String): Decoder[Thing] = Decoder { _: HCursor => Left(DecodingFailure(msg, List.empty)) }
-
- def decoder(p: Predicate): Decoder[_] = p match {
- case p: Plain => plainDataDecoder(p)
- case Enlist(of) => Decoder.decodeSeq(decoder(of))
- case Index(Text(_), over) => Decoder.decodeMap(KeyDecoder.decodeKeyString, decoder(over))
- case t: Type => dataTypeDecoder(t)
- case Link(p: Path, t: Type) => dataTypeDecoder(t)
- case Link(l, _) if l == ID("types") / "Location" => locDecoder
- case Link(l, _) if l == ID("Location") => locDecoder
- case Link(p: Path, any: AnyOf) => //TODO check actual
- val options: Map[Location, Link] = any.p.collect {
- case l@Link(id: ID, Nothing) => id -> l
- case l@Link(pa: Path, Nothing) => pa.last -> l
- }.toMap
-
- Decoder.decodeString.map { s =>
- options.get(Location(s)) match {
- case Some(found) => found
- case None =>
- DecodingFailure.fromThrowable(
- new IllegalArgumentException(s"$s Not a value from AnyOf in $p"), List.empty)
- }
- }
- case Link(p: Path, other) =>
- throw new IllegalArgumentException(s"On $p not a type: $other")
- case many: AnyOf => anyDecoder(many)
-
- case Nothing => Decoder.apply(_ => Right(Nothing))
- case Anything => throw new IllegalArgumentException("Not supported, use AnyOf() instead")
- case _ => ??? //TODO Predicate as AnyOf()
- }
-
- def dataTypeDecoder(t: Type): Decoder[Data] = { c: HCursor =>
- t.fields.map {
- case Field(name, p) => c.downField(name).as(decoder(p))
- }.foldRight(Right(scala.Nil): Either[DecodingFailure, scala.List[_]]) {
- (e, acc) => for (xs <- acc.right; x <- e.right) yield x :: xs
- }.map { v => Data(t, v) }
- }
-
- private val plainDataDecoder: PartialFunction[Plain, Decoder[_]] = {
- case Bool => Decoder.decodeBoolean
- case Text(_) => Decoder.decodeString
- case Number(_, _) => Decoder.decodeLong
- case Date => Decoder.decodeString.map(sql.Date.valueOf)
- case Time => Decoder.decodeString.map(sql.Time.valueOf)
- case Timestamp => Decoder.decodeString.map(sql.Timestamp.valueOf)
- case other => throw new IllegalArgumentException(s"Not supported: $other")
- }
-
- def anyDecoder(many: AnyOf): Decoder[_] = { c: HCursor =>
- c.downField(discriminator).as[String].flatMap { name =>
- many.p.collect {
- case Link(p: Path, t: Type) if p.ids.last.key == name => dataTypeDecoder(t).apply(c)
- case l@Link(p: Path, Nothing) if p.ids.last.key == name => Right(l) //use links for enums
- }.head
- }
- }
-
- def locKeyDecoder: KeyDecoder[Location] = KeyDecoder { s =>
- Location(s) match {
- case Root => Some(Root)
- case id: ID => Some(id)
- case path: Path => Some(path)
- case _ => None
- }
- }
-
- def idKeyDecoder: KeyDecoder[ID] = KeyDecoder { k => Some(ID(k)) }
-
- def locDecoder: Decoder[Location] = Decoder.decodeString.map { s =>
- Location(s) match {
- case Root => Root
- case id: ID => id
- case path: Path => path
- case _ => throw new IllegalArgumentException("Not a location")
- }
- }
-}
diff --git a/http/src/main/scala/dev/rudiments/hardcore/http/ThingEncoder.scala b/http/src/main/scala/dev/rudiments/hardcore/http/ThingEncoder.scala
deleted file mode 100644
index dd30abe1..00000000
--- a/http/src/main/scala/dev/rudiments/hardcore/http/ThingEncoder.scala
+++ /dev/null
@@ -1,218 +0,0 @@
-package dev.rudiments.hardcore.http
-
-import dev.rudiments.hardcore.Initial.types
-import dev.rudiments.hardcore._
-import io.circe.{Json, KeyEncoder}
-
-import java.sql
-
-object ThingEncoder {
- val codecs: ID = ID("codecs")
- val jsonCodec: Location = codecs / "json"
-
- def init(ctx: Node): Commit = {
- val tx = new Tx(ctx)
- tx += codecs -> Node.empty
- tx += jsonCodec -> Node(leafIs = Internal)
-
- val foundTypes = ctx ??* types match {
- case Found(_, values) => values
- case other => throw new IllegalStateException(s"Can't read /types, got $other")
- }
-
- foundTypes
-
- val prepared = tx.>>
- prepared match {
- case Prepared(c) => ctx << c match {
- case Committed(cmt) =>
- cmt
- case _ => throw new IllegalStateException("Json Encoder commit failed")
- }
- case _ => throw new IllegalStateException("Json Encoder commit not prepared")
- }
- }
-
- val discriminator = "type"
- val partners: Location = ID("Partners")
-
- implicit val idEncoder: KeyEncoder[ID] = KeyEncoder.encodeKeyString.contramap(id => id.key.toString)
- implicit val pathEncoder: KeyEncoder[Path] = KeyEncoder.encodeKeyString.contramap(path => path.toString)
-
- def encodeData(data: Data): Json = encode(data.what, data.data)
-
- def encodeNode(node: Node): Json = {
- val selfJson = Seq(
- "self" -> node.self,
- "key-is" -> node.keyIs,
- "leaf-is" -> node.leafIs,
- ).collect {
- case (s, v) if v != Nothing => s -> encodeAnything(v)
- }
-
- val leafs = node.leafs.toSeq.map { case (k, v) => idEncoder(k) -> encodeAnything(v) }
- val branches = node.branches.toSeq.map { case (k, v) => idEncoder(k) -> encodeNode(v) }
-
- val leafJson = if(leafs.nonEmpty) {
- Seq("leafs" -> Json.obj(leafs: _*))
- } else {
- Seq.empty
- }
-
- val branchesJson = if(branches.nonEmpty) {
- Seq("branches" -> Json.obj(branches: _*))
- } else {
- Seq.empty
- }
-
- val all = Seq("type" -> Json.fromString("Node")) ++ selfJson ++ leafJson ++ branchesJson
-
- Json.obj(all: _*)
- }
-
- def encodeAnything(thing: Thing): Json = thing match {
- case Data(p, v) => encode(p, v)
- case o: CRUD.O => encodeOut(o)
- case p: Predicate => encodePredicate(p)
- case c: Commit => Json.obj(discriminator -> Json.fromString("Commit"), "crud" -> encodeNode(c.crudNode()))
- case n: Node => encodeNode(n)
- case other =>
- Json.fromString(s"NOT IMPLEMENTED something: $other")
- }
-
- def encode(p: Predicate, v: Any): Json = (p, v) match {
- case (Link(_, any: AnyOf), l: Link) =>
- val found = any.p.collect {
- case f: Link if f == l => f
- }
- if(found.size == 1) {
- Json.fromString(found.head.where.lastString)
- } else {
- throw new IllegalArgumentException(s"Linked $l link not in AnyOf")
- }
- case (loc: Link, l: Location) if loc.where == types / "Location" =>
- Json.fromString(l.toString)
- case (l: Link, values) =>
- encode(l.what, values) //TODO add 'type' from Link's location
- case (t: Type, values: Seq[Any]) =>
- Json.obj(t.fields.zip(values).map { case (f, v) => (f.name, encode(f.of, v)) }:_*)
- case (p: Plain, v: Any) => encodePlain(p, v)
- case (Enlist(p), vs: Seq[Any]) =>
- Json.arr(vs.map(v => encode(p, v)):_*)
- case (Index(_, pv), vs: Map[Location, Any]) => Json.obj(
- vs.toSeq.map { case (k, v) => k.toString -> encode(pv, v) } :_*
- )
- case (a: AnyOf, v: Link) if a.p.contains(v) => Json.fromString(v.where.toString)
-
- case (other, another) =>
- Json.fromString(s"NOT IMPLEMENTED: $another")
- }
-
- def encodePlain(p: Plain, v: Any): Json = (p, v) match {
- case (Text(_), s: String) => Json.fromString(s)
- case (Number(_, _), i: Int) => Json.fromInt(i)
- case (Number(_, _), l: Long) => Json.fromLong(l)
- case (Bool, b: Boolean) => Json.fromBoolean(b)
- case (Binary, Nothing) => Json.fromString("∅")
- case (Binary, _) => Json.fromString("--BINARY--")
- case (Date, d: sql.Date) => Json.fromString(d.toString)
- case (_, None) => Json.Null
- case (_, _) => throw new IllegalArgumentException(s"Can't encode [$v] of $p ")
- }
-
- def encodeOut(out: CRUD.O): Json = out match {
- case evt: CRUD.Evt => encodeEvent(evt)
- case Readen(Data(Link(l, p), v)) =>
- Json.obj(
- discriminator -> Json.fromString(l.toString),
- "thing" -> encode(p, v)
- )
- case Readen(Link(l, p)) =>
- p match {
- case Anything | Nothing => Json.obj(
- discriminator -> Json.fromString(l.toString)
- )
- case other => Json.obj(
- discriminator -> Json.fromString(l.toString),
- "thing" -> encodePredicate(other)
- )
- }
- case Readen(p: Predicate) => encodePredicate(p)
- case Readen(t) => Json.obj(
- discriminator -> Json.fromString("?"),
- "thing" -> encodeAnything(t)
- )
- case NotExist => Json.fromString("NotExist")
- case NotImplemented => Json.obj("type" -> Json.fromString("NotImplemented"))
- case Prepared(cmt) => Json.obj(
- discriminator -> Json.fromString("Prepared"),
- "CRUD" -> encodeNode(cmt.crudNode())
- )
- case other =>
- Json.fromString(s"NOT IMPLEMENTED Out: $other")
- }
-
- def encodeEvent(evt: CRUD.Evt): Json = evt match {
- case Created(t) => Json.obj(
- discriminator -> Json.fromString("Created"),
- "data" -> encodeAnything(t)
- )
- case Updated(o, n) => Json.obj(
- discriminator -> Json.fromString("Updated"),
- "new" -> encodeAnything(n),
- "old" -> encodeAnything(o)
- )
- case Deleted(d) => Json.obj(
- discriminator -> Json.fromString("Deleted"),
- "old" -> encodeAnything(d)
- )
- case Committed(cmt) => Json.obj(
- discriminator -> Json.fromString("Committed"),
- "CRUD" -> encodeNode(cmt.crudNode())
- )
- }
-
- def encodePredicate(p: Predicate): Json = p match {
- case Anything => Json.obj(discriminator -> Json.fromString("Anything"))
- case Nothing => Json.obj(discriminator -> Json.fromString("Nothing"))
- case p: Plain => p match {
- case Number(from, upTo) => Json.obj(
- "type" -> Json.fromString("Number"),
- "from" -> Json.fromString(from.toString),
- "up-to" -> Json.fromString(upTo.toString)
- )
- case Text(maxSize) => Json.obj(
- "type" -> Json.fromString("Text"),
- "max-size" -> Json.fromString(maxSize.toString),
- )
- case Bool => Json.obj(discriminator -> Json.fromString("Bool"))
- case Binary => Json.obj(discriminator -> Json.fromString("Binary"))
- case other => Json.fromString(s"OTHER PREDICATE: $other")
- }
- case t: Type => Json.obj((discriminator -> Json.fromString("Type")) +: t.fields.map{ f => f.name -> encodePredicate(f.of) } :_*)
- case Declared(l) => Json.obj(discriminator -> Json.fromString(l.lastString))
- case Enlist(p) => Json.obj(discriminator -> Json.fromString("Enlist"), "of" -> encodePredicate(p))
- case Index(k, v) => Json.obj(discriminator -> Json.fromString("Index"), "of" -> encodePredicate(k), "over" -> encodePredicate(v))
- case Link(l, p) => p match {
- case Anything | Nothing | Bool => Json.fromString(l.lastString)
- case _: Type => Json.obj(discriminator -> Json.fromString(l.lastString))
- case _: Declared => Json.obj(discriminator -> Json.fromString(l.lastString))
- case _: AnyOf => Json.obj(discriminator -> Json.fromString(l.lastString))
- case other =>
- Json.fromString(s"NOT IMPLEMENTED Predicate: $other")
- }
- case a: AnyOf =>
- val links = a.p.collect { case l: Link => l.where }.toSeq
- if(a.p.size == links.size) { // AnyOf(Link*)
- Json.obj(discriminator -> Json.fromString("AnyOf"), "p" -> encodeEnum(links))
- } else {
- ???
- }
- case other =>
- Json.fromString(s"NOT IMPLEMENTED Predicate: $other")
- }
-
- def encodeEnum(values: Seq[Location]): Json = {
- Json.arr(values.map(v => Json.fromString(v.lastString)): _*)
- }
-}
diff --git a/http/src/test/resources/application.conf b/http/src/test/resources/application.conf
deleted file mode 100644
index 92ba6f8f..00000000
--- a/http/src/test/resources/application.conf
+++ /dev/null
@@ -1,16 +0,0 @@
-http {
- prefix = "api"
-
- host = "localhost"
- port = 8080
-
- akka-http-cors {
- allow-generic-http-requests = true
- allow-credentials = true
- allowed-origins = ["*"]
- allowed-headers = ["*"]
- allowed-methods = ["GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE", "UPGRADE"]
- exposed-headers = ["X-Tx"]
- max-age = 30 m
- }
-}
\ No newline at end of file
diff --git a/http/src/test/resources/logback.xml b/http/src/test/resources/logback.xml
deleted file mode 100644
index a994bdfc..00000000
--- a/http/src/test/resources/logback.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- %d{HH:mm:ss.SSS} %-5level %-20logger{20} %msg%n
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/http/src/test/scala/test/dev/rudiments/http/ScalaRouterSpec.scala b/http/src/test/scala/test/dev/rudiments/http/ScalaRouterSpec.scala
deleted file mode 100644
index 267713f0..00000000
--- a/http/src/test/scala/test/dev/rudiments/http/ScalaRouterSpec.scala
+++ /dev/null
@@ -1,98 +0,0 @@
-package test.dev.rudiments.http
-
-import akka.actor.ActorSystem
-import akka.http.scaladsl.model.StatusCodes
-import akka.http.scaladsl.testkit.ScalatestRouteTest
-import dev.rudiments.hardcore.Initial.types
-import dev.rudiments.hardcore._
-import dev.rudiments.hardcore.http.{CirceSupport, ScalaRouter, ThingDecoder}
-import io.circe.Decoder
-import org.junit.runner.RunWith
-import org.scalatest.matchers.should.Matchers
-import org.scalatest.wordspec.AnyWordSpec
-import org.scalatestplus.junit.JUnitRunner
-
-@RunWith(classOf[JUnitRunner])
-class ScalaRouterSpec extends AnyWordSpec with Matchers with ScalatestRouteTest with CirceSupport {
- private implicit val actorSystem: ActorSystem = ActorSystem()
- private val t = Type(
- Field("id", Number(Long.MinValue, Long.MaxValue)),
- Field("name", Text(Int.MaxValue)),
- Field("comment", Text(Int.MaxValue))
- )
-
- private val mem = new Memory()
- private val ts = new TypeSystem(mem /! types)
- private val td = new ThingDecoder(ts)
- private val router = new ScalaRouter(mem.node)(td)
- private val routes = router.seal()
- private implicit val de: Decoder[Data] = td.dataTypeDecoder(t)
-
- mem += ID("example") -> Node(Nothing, leafIs = t)
- mem += ID("34") -> Node(Nothing, leafIs = Nothing)
- mem += (ID("34") / "43") -> Node(Nothing, leafIs = t)
-
- private val sample = t.data(42, "sample", "non-optional comment")
-
- "no element by ID" in {
- Get("/example/42") ~> routes ~> check {
- response.status should be (StatusCodes.NotFound)
- mem ? ID("42") should be (NotExist)
- }
- }
-
- "put item into repository" in {
- Post("/example/42", sample) ~> routes ~> check {
- response.status should be (StatusCodes.Created)
- responseAs[Data] should be (sample)
- }
- mem ? (ID("example") / "42") should be (Readen(sample))
-
- Get("/example/42") ~> routes ~> check {
- response.status should be (StatusCodes.OK)
- responseAs[Data] should be (sample)
- }
- }
-
- "update item in repository" in {
- Put("/example/42", t.data(42L, "test", "non-optional comment")) ~> routes ~> check {
- response.status should be (StatusCodes.OK)
- responseAs[Data] should be (t.data(42L, "test", "non-optional comment"))
- }
- Get("/example/42") ~> routes ~> check {
- response.status should be (StatusCodes.OK)
- responseAs[Data] should be (t.data(42L, "test", "non-optional comment"))
- }
- }
-
- "second POST with same item conflicts with existing" in {
- Post("/example/42", t.data(42L, "test", "non-optional comment")) ~> routes ~> check {
- response.status should be (StatusCodes.Conflict)
- }
- }
-
- "delete items from repository" in {
- Delete("/example/42") ~> routes ~> check {
- response.status should be (StatusCodes.NoContent)
- }
- Get("/example/42") ~> routes ~> check {
- response.status should be (StatusCodes.NotFound)
- }
- }
-
- "can create deep into memory" in {
- val sample2 = t.data(0L, "deep", "test")
-
- val path = ID("34") / "43" / "10"
-
- Get("/34/43") ~> routes ~> check {
- response.status should be(StatusCodes.OK)
- }
-
- Post("/34/43/10", sample2) ~> routes ~> check {
- response.status should be (StatusCodes.Created)
- responseAs[Data] should be (sample2)
- mem ? path should be (Readen(sample2))
- }
- }
-}
diff --git a/http/src/test/scala/test/dev/rudiments/http/Smt.scala b/http/src/test/scala/test/dev/rudiments/http/Smt.scala
deleted file mode 100644
index 16fd3a23..00000000
--- a/http/src/test/scala/test/dev/rudiments/http/Smt.scala
+++ /dev/null
@@ -1,13 +0,0 @@
-package test.dev.rudiments.http
-
-sealed trait Blah {}
-
-case class Smt(
- id: Long,
- name: String,
- comment: Option[String] = None
-) extends Blah
-
-case class Thng(
- code: String
-) extends Blah
\ No newline at end of file
diff --git a/http/src/test/scala/test/dev/rudiments/http/ThingDecoderSpec.scala b/http/src/test/scala/test/dev/rudiments/http/ThingDecoderSpec.scala
deleted file mode 100644
index 69aaaa75..00000000
--- a/http/src/test/scala/test/dev/rudiments/http/ThingDecoderSpec.scala
+++ /dev/null
@@ -1,49 +0,0 @@
-package test.dev.rudiments.http
-
-import dev.rudiments.hardcore.Initial.types
-import dev.rudiments.hardcore._
-import dev.rudiments.hardcore.http.{CirceSupport, ScalaRouter, ThingDecoder}
-import io.circe.{Decoder, Json}
-import org.junit.runner.RunWith
-import org.scalatest.matchers.should.Matchers
-import org.scalatest.wordspec.AnyWordSpec
-import org.scalatestplus.junit.JUnitRunner
-
-@RunWith(classOf[JUnitRunner])
-class ThingDecoderSpec extends AnyWordSpec with Matchers with CirceSupport {
- private val t = Type(
- Field("id", Number(Long.MinValue, Long.MaxValue)),
- Field("name", Text(Int.MaxValue)),
- Field("comment", Text(Int.MaxValue))
- )
-
- private val mem = new Memory()
- private val ts = new TypeSystem(mem /! types)
- private val td = new ThingDecoder(ts)
- private val router = new ScalaRouter(mem.node)(td)
- private val de: Decoder[Data] = td.dataTypeDecoder(t)
-
- "data decoder can decode" in {
- de.decodeJson(Json.obj(
- "id" -> Json.fromInt(42),
- "name" -> Json.fromString("sample"),
- "comment" -> Json.fromString("non-optional comment")
- )) should be (Right(t.data(42, "sample", "non-optional comment")))
- }
-
- "can encode and decode empty Node" ignore {
- val encoded = thingEncoder(Node.empty)
- encoded should be (Json.obj(
- "type" -> Json.fromString("Node"),
- "self" -> Json.obj("type" -> Json.fromString("Nothing")),
- "leaf-is" -> Json.obj("type" -> Json.fromString("Anything")),
- "key-is" -> Json.obj(
- "type" -> Json.fromString("Text"),
- "maxSize" -> Json.fromString("1024")
- )
- ))
-
- val decoded = de.decodeJson(encoded)
- decoded should be (Node.empty)
- }
-}
diff --git a/http/src/test/scala/test/dev/rudiments/http/ThingEncoderSpec.scala b/http/src/test/scala/test/dev/rudiments/http/ThingEncoderSpec.scala
deleted file mode 100644
index 99eba37d..00000000
--- a/http/src/test/scala/test/dev/rudiments/http/ThingEncoderSpec.scala
+++ /dev/null
@@ -1,72 +0,0 @@
-package test.dev.rudiments.http
-
-import dev.rudiments.hardcore._
-import dev.rudiments.hardcore.http.ThingEncoder.encodeNode
-import dev.rudiments.hardcore.Initial.types
-import dev.rudiments.hardcore.http.{CirceSupport, ScalaRouter, ThingDecoder}
-import io.circe.Json
-import org.junit.runner.RunWith
-import org.scalatest.matchers.should.Matchers
-import org.scalatest.wordspec.AnyWordSpec
-import org.scalatestplus.junit.JUnitRunner
-
-@RunWith(classOf[JUnitRunner])
-class ThingEncoderSpec extends AnyWordSpec with Matchers with CirceSupport {
- private val ctx: Memory = new Memory()
- private val t = Type(
- Field("id", Number(Long.MinValue, Long.MaxValue)),
- Field("name", Text(Int.MaxValue)),
- Field("comment", Text(Int.MaxValue))
- )
-
- private val mem = new Memory()
- private val ts = new TypeSystem(mem /! types)
- private val td = new ThingDecoder(ts)
- private val router = new ScalaRouter(mem.node)(td)
-
- private val initial = ctx ?? ID("commits") match {
- case Found(_, values) => if(values.size == 1) {
- values.head._2 match {
- case c: Commit =>
- c
- case other => fail(s"Expecting commit, got $other")
- }
- } else {
- fail(s"Expecting 1 initial commit, got ${values.size}")
- }
- }
-
- "can encode links" in {
- router.thingEncoder(ctx ! (types / "Bool")) should be (Json.fromString("Bool"))
- router.thingEncoder(ctx ! (types / "Number")) should be (Json.obj(
- "type" -> Json.fromString("Number")
- ))
- }
-
- "can encode predicates" in {
- router.thingEncoder(ctx ? (types / "Bool")) should be(Json.obj(
- "type" -> Json.fromString("Nothing")
- ))
- router.thingEncoder(ctx ? (types / "Number")) should be(Json.obj(
- "type" -> Json.fromString("Type"),
- "from" -> Json.obj("type" -> Json.fromString("Anything")),
- "to" -> Json.obj("type" -> Json.fromString("Anything"))
- ))
- }
-
- "can encode first commit" in {
- router.thingEncoder(initial) should be(Json.obj(
- "type" -> Json.fromString("Commit"),
- "crud" -> encodeNode(Node.fromMap(initial.crud))
- )
- )
- }
-
- "dataEncoder can encode" in {
- router.thingEncoder(t.data(42, "sample", None)) should be (Json.obj(
- "id" -> Json.fromInt(42),
- "name" -> Json.fromString("sample"),
- "comment" -> Json.Null
- ))
- }
-}
diff --git a/management/build.gradle b/management/build.gradle
deleted file mode 100644
index 378848af..00000000
--- a/management/build.gradle
+++ /dev/null
@@ -1,3 +0,0 @@
-dependencies {
- implementation project(':core')
-}
\ No newline at end of file
diff --git a/management/src/main/scala/dev/rudiments/management/Management.scala b/management/src/main/scala/dev/rudiments/management/Management.scala
deleted file mode 100644
index e89ae156..00000000
--- a/management/src/main/scala/dev/rudiments/management/Management.scala
+++ /dev/null
@@ -1,55 +0,0 @@
-package dev.rudiments.management
-
-import dev.rudiments.hardcore._
-import dev.rudiments.hardcore.Initial.types
-
-object Management {
- val work: Location = ID("work")
- val team: Location = work / "team"
- val tasks: Location = work / "tasks"
- val boards: Location = work / "boards"
- val docs: Location = work / "docs"
- val meetings: Location = work / "meetings"
-
- def init(ctx: Node): Commit = {
- val tx = new Tx(ctx)
-
- tx += types / "User" -> Type(
- Field("name", Text(1024)),
- Field("email", Text(1024))
- )
- tx += types / "TaskStatus" -> Node.partnership(types, Seq("TODO", "InProgress", "Done"))
- tx += types / "TODO" -> Nothing
- tx += types / "InProgress" -> Nothing
- tx += types / "Done" -> Nothing
- tx += types / "Task" -> Type(
- Field("name", Text(4096)),
- Field("summary", Text(4 * 1024 * 1024)),
- Field("deadline", Date),
- Field("status", tx ! (types / "TaskStatus"))
- )
- tx += types / "BoardColumn" -> Type(
- Field("tasks", Enlist(tx ! (types / "Location")))
- )
-
- tx += work -> Node.empty
- tx += team -> Node(leafIs = tx ! (types / "User"))
- tx += tasks -> Node(leafIs = tx ! (types / "Task"))
- tx += boards -> Node(leafIs = tx ! (types / "BoardColumn"))
- tx += docs -> Node.empty
- tx += meetings -> Node.empty
-
- val prepared = tx.>>
-
- prepared match {
- case Prepared(c) => ctx << c match {
- case Committed(cmt) =>
- cmt
- case _ => throw new IllegalStateException("Management commit failed")
- }
- case _ => throw new IllegalStateException("Management commit not prepared")
- }
- }
-
-
-}
diff --git a/management/src/test/scala/test/dev/rudiments/management/ManagementSpec.scala b/management/src/test/scala/test/dev/rudiments/management/ManagementSpec.scala
deleted file mode 100644
index 698c4747..00000000
--- a/management/src/test/scala/test/dev/rudiments/management/ManagementSpec.scala
+++ /dev/null
@@ -1,46 +0,0 @@
-package test.dev.rudiments.management
-
-import dev.rudiments.hardcore.Initial.types
-import dev.rudiments.hardcore._
-import dev.rudiments.management.Management
-import org.junit.runner.RunWith
-import org.scalatest.matchers.should.Matchers
-import org.scalatest.wordspec.AnyWordSpec
-import org.scalatestplus.junit.JUnitRunner
-
-import scala.collection.mutable
-
-@RunWith(classOf[JUnitRunner])
-class ManagementSpec extends AnyWordSpec with Matchers {
- private val mem: Memory = new Memory()
- Management.init(mem.node)
-
- "all types should exist" in {
- mem ? (types / "User") should be(Readen(Type(Field("name", Text(1024)), Field("email", Text(1024)))))
- mem ? (types / "Task") should be(Readen(Type(
- Field("name", Text(4096)),
- Field("summary", Text(4 * 1024 * 1024)),
- Field("deadline", Date),
- Field("status", mem ! (types / "TaskStatus"))
-// Field("assigned", Link(types / "User", Type(Field("name", Text(1024)))))
- )))
-
- val readen = mem ? (types / "BoardColumn")
- val expected = Readen(Type(
- Field("tasks", Enlist(mem ! (types / "Location")))
- ))
- readen should be (expected)
- }
-
- "all locations should exist" in {
- mem ? Management.work should be (Readen(Node(
- branches = mutable.Map(
- ID("team") -> Node(leafIs = mem ! types / "User"),
- ID("tasks") -> Node(leafIs = mem ! types / "Task"),
- ID("boards") -> Node(leafIs = mem ! types / "BoardColumn"), //TODO check node constraint
- ID("docs") -> Node.empty,
- ID("meetings") -> Node.empty
- )
- )))
- }
-}
diff --git a/settings.gradle b/settings.gradle
index 068d433d..ad008c53 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,7 +1,6 @@
+plugins {
+ id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0'
+}
+
rootProject.name = 'hardcore'
-include 'core'
-include 'http'
-include 'file'
-include 'sql'
-include 'management'
-include 'example'
+include('core', 'file', 'git', 'example')
\ No newline at end of file
diff --git a/sql/build.gradle b/sql/build.gradle
deleted file mode 100644
index 9a97390c..00000000
--- a/sql/build.gradle
+++ /dev/null
@@ -1,7 +0,0 @@
-dependencies {
- implementation 'org.scalikejdbc:scalikejdbc_2.13:4.0.0'
-
- testImplementation 'com.dimafeng:testcontainers-scala_2.13:0.39.12'
- testImplementation 'com.dimafeng:testcontainers-scala-postgresql_2.13:0.39.12'
- testImplementation 'org.postgresql:postgresql:42.3.1'
-}
\ No newline at end of file
diff --git a/sql/src/main/scala/dev/rudiments/hardcore/sql/PostgresAdapter.scala b/sql/src/main/scala/dev/rudiments/hardcore/sql/PostgresAdapter.scala
deleted file mode 100644
index 3f4f98e0..00000000
--- a/sql/src/main/scala/dev/rudiments/hardcore/sql/PostgresAdapter.scala
+++ /dev/null
@@ -1,5 +0,0 @@
-package dev.rudiments.hardcore.sql
-
-class PostgresAdapter {
-
-}
diff --git a/sql/src/test/resources/logback.xml b/sql/src/test/resources/logback.xml
deleted file mode 100644
index a994bdfc..00000000
--- a/sql/src/test/resources/logback.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- %d{HH:mm:ss.SSS} %-5level %-20logger{20} %msg%n
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/target.md b/target.md
new file mode 100644
index 00000000..a70b28ae
--- /dev/null
+++ b/target.md
@@ -0,0 +1,58 @@
+### Concept
+Protocol and implementation to store and transfer:
+- variable lot of files as one
+- their meta-data
+ - list of content;
+ - checksums (hash, signature);
+ - dependencies like schemas and prev versions;
+- and their log as CUD operations
+ - Create with content;
+ - Update with new content and dependency or both old and new data;
+ - Delete with dependency or old data;
+- custom content in same manner: tables; trees; comments.
+
+Header should be relatively compact. Content should be friendly for streaming and batch/chunk transfer.
+
+### Technological targets
+#### git integration
+Be able to fully read 100+ of top git repositories, including
+- git
+- scala library
+- go lang
+- node.js
+
+Be able to prepare commits and transfer packs in git format (two-way integration).
+
+At first, be able to repeat git-like commands:
+- add
+- commit
+- delete
+- pull
+- push
+- rebase
+
+#### scala integration
+Be able to parse 10+ popular scala projects - their structure and evolution by-commit:
+- classes (including case classes), traits, objects, enums
+- fields, methods and their signatures (including type-parameters)
+- locations - modules, packages, files
+- nested things (identifiable)
+
+#### jvm integration
+Be able to parse jar-file same way as any project.
+
+
+#### Competitor to excel
+Minimal requirements and data:
+- semi-free cell space and regions (addressable fields);
+- inference of cell types, lazy for single cell, but with a way to constrain a region;
+- reactive inference of formulas between cells;
+- a way to store non-cellular structures (trees, lists, ie enums);
+- a region can be a table, as a strict constraint;
+- composite regions with common constraints;
+- propagation of constraints from outer regions to the cell;
+- reference enforcement;
+- unique and order enforcement;
+- styles, including computable and relative;
+- non-2D spaces, projections to 2D and maps?
+- multi-indexes for 2+D?
\ No newline at end of file