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 ![Rudiments build](https://github.com/rudiments-dev/hardcore/workflows/Rudiments%20repository%20builds%20and%20coverage%20reports/badge.svg?branch=develop) -[![codecov](https://codecov.io/gh/rudiments-dev/hardcore/branch/develop/graph/badge.svg)](https://codecov.io/gh/rudiments-dev/hardcore) +[![codecov](https://codecov.io/gh/rudiments-dev/hardcore/graph/badge.svg?token=NG67EUNPTZ)](https://codecov.io/gh/rudiments-dev/hardcore) [![Maven Central](https://img.shields.io/maven-central/v/dev.rudiments/implementation.svg?label=Maven%20Central)](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