From a6c6f55c4050ba2b935f446b3e46e89ec99c5fe9 Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 23 Feb 2023 07:48:03 +0600 Subject: [PATCH 01/75] drop --- build.gradle | 7 +- {file => codec}/build.gradle | 3 - .../dev/rudiments/hardcore/AgentCrud.scala | 53 --- .../scala/dev/rudiments/hardcore/CRUD.scala | 57 --- .../dev/rudiments/hardcore/Initial.scala | 150 ------- .../scala/dev/rudiments/hardcore/Memory.scala | 75 ---- .../scala/dev/rudiments/hardcore/Node.scala | 365 ------------------ .../scala/dev/rudiments/hardcore/Thing.scala | 213 ---------- .../scala/dev/rudiments/hardcore/Tx.scala | 127 ------ .../dev/rudiments/hardcore/TypeSystem.scala | 76 ---- .../test/scala/test/dev/rudiments/Smt.scala | 13 - .../dev/rudiments/hardcore/BulkTest.scala | 53 --- .../dev/rudiments/hardcore/LocationSpec.scala | 42 -- .../dev/rudiments/hardcore/MemorySpec.scala | 29 -- .../dev/rudiments/hardcore/NodeSpec.scala | 28 -- .../test/dev/rudiments/hardcore/TxSpec.scala | 95 ----- .../rudiments/hardcore/TypeSystemSpec.scala | 24 -- example/build.gradle | 13 +- example/example-file.http | 34 -- example/example.http | 31 -- example/routers.http | 17 - .../main/scala/dev/rudiments/app/Main.scala | 39 -- .../test/dev/rudiments/app/BoardSpec.scala | 136 ------- .../test/dev/rudiments/app/TasksSpec.scala | 108 ------ example/types.http | 22 -- .../dev/rudiments/hardcore/file/File.scala | 29 -- .../rudiments/hardcore/file/FileAgent.scala | 248 ------------ .../rudiments/hardcore/file/Messages.scala | 22 -- file/src/test/resources/application.conf | 16 - file/src/test/resources/file-test/24.bin | 1 - file/src/test/resources/file-test/42.json | 3 - .../file-test/folder1/folder2/123.json | 3 - file/src/test/resources/logback.xml | 14 - .../rudiments/hardcore/file/FileSpec.scala | 153 -------- http/build.gradle | 13 - .../hardcore/http/CirceSupport.scala | 14 - .../rudiments/hardcore/http/RootRouter.scala | 78 ---- .../rudiments/hardcore/http/ScalaRouter.scala | 94 ----- .../rudiments/hardcore/http/ScalaTypes.scala | 21 - .../hardcore/http/ThingDecoder.scala | 284 -------------- .../hardcore/http/ThingEncoder.scala | 218 ----------- http/src/test/resources/application.conf | 16 - http/src/test/resources/logback.xml | 14 - .../dev/rudiments/http/ScalaRouterSpec.scala | 98 ----- .../scala/test/dev/rudiments/http/Smt.scala | 13 - .../dev/rudiments/http/ThingDecoderSpec.scala | 49 --- .../dev/rudiments/http/ThingEncoderSpec.scala | 72 ---- management/build.gradle | 3 - .../dev/rudiments/management/Management.scala | 55 --- .../rudiments/management/ManagementSpec.scala | 46 --- settings.gradle | 5 +- sql/build.gradle | 7 - .../hardcore/sql/PostgresAdapter.scala | 5 - sql/src/test/resources/logback.xml | 14 - 54 files changed, 3 insertions(+), 3415 deletions(-) rename {file => codec}/build.gradle (72%) delete mode 100644 core/src/main/scala/dev/rudiments/hardcore/AgentCrud.scala delete mode 100644 core/src/main/scala/dev/rudiments/hardcore/CRUD.scala delete mode 100644 core/src/main/scala/dev/rudiments/hardcore/Initial.scala delete mode 100644 core/src/main/scala/dev/rudiments/hardcore/Memory.scala delete mode 100644 core/src/main/scala/dev/rudiments/hardcore/Node.scala delete mode 100644 core/src/main/scala/dev/rudiments/hardcore/Thing.scala delete mode 100644 core/src/main/scala/dev/rudiments/hardcore/Tx.scala delete mode 100644 core/src/main/scala/dev/rudiments/hardcore/TypeSystem.scala delete mode 100644 core/src/test/scala/test/dev/rudiments/Smt.scala delete mode 100644 core/src/test/scala/test/dev/rudiments/hardcore/BulkTest.scala delete mode 100644 core/src/test/scala/test/dev/rudiments/hardcore/LocationSpec.scala delete mode 100644 core/src/test/scala/test/dev/rudiments/hardcore/MemorySpec.scala delete mode 100644 core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala delete mode 100644 core/src/test/scala/test/dev/rudiments/hardcore/TxSpec.scala delete mode 100644 core/src/test/scala/test/dev/rudiments/hardcore/TypeSystemSpec.scala delete mode 100644 example/example-file.http delete mode 100644 example/example.http delete mode 100644 example/routers.http delete mode 100644 example/src/main/scala/dev/rudiments/app/Main.scala delete mode 100644 example/src/test/scala/test/dev/rudiments/app/BoardSpec.scala delete mode 100644 example/src/test/scala/test/dev/rudiments/app/TasksSpec.scala delete mode 100644 example/types.http delete mode 100644 file/src/main/scala/dev/rudiments/hardcore/file/File.scala delete mode 100644 file/src/main/scala/dev/rudiments/hardcore/file/FileAgent.scala delete mode 100644 file/src/main/scala/dev/rudiments/hardcore/file/Messages.scala delete mode 100644 file/src/test/resources/application.conf delete mode 100644 file/src/test/resources/file-test/24.bin delete mode 100644 file/src/test/resources/file-test/42.json delete mode 100644 file/src/test/resources/file-test/folder1/folder2/123.json delete mode 100644 file/src/test/resources/logback.xml delete mode 100644 file/src/test/scala/test/dev/rudiments/hardcore/file/FileSpec.scala delete mode 100644 http/build.gradle delete mode 100644 http/src/main/scala/dev/rudiments/hardcore/http/CirceSupport.scala delete mode 100644 http/src/main/scala/dev/rudiments/hardcore/http/RootRouter.scala delete mode 100644 http/src/main/scala/dev/rudiments/hardcore/http/ScalaRouter.scala delete mode 100644 http/src/main/scala/dev/rudiments/hardcore/http/ScalaTypes.scala delete mode 100644 http/src/main/scala/dev/rudiments/hardcore/http/ThingDecoder.scala delete mode 100644 http/src/main/scala/dev/rudiments/hardcore/http/ThingEncoder.scala delete mode 100644 http/src/test/resources/application.conf delete mode 100644 http/src/test/resources/logback.xml delete mode 100644 http/src/test/scala/test/dev/rudiments/http/ScalaRouterSpec.scala delete mode 100644 http/src/test/scala/test/dev/rudiments/http/Smt.scala delete mode 100644 http/src/test/scala/test/dev/rudiments/http/ThingDecoderSpec.scala delete mode 100644 http/src/test/scala/test/dev/rudiments/http/ThingEncoderSpec.scala delete mode 100644 management/build.gradle delete mode 100644 management/src/main/scala/dev/rudiments/management/Management.scala delete mode 100644 management/src/test/scala/test/dev/rudiments/management/ManagementSpec.scala delete mode 100644 sql/build.gradle delete mode 100644 sql/src/main/scala/dev/rudiments/hardcore/sql/PostgresAdapter.scala delete mode 100644 sql/src/test/resources/logback.xml diff --git a/build.gradle b/build.gradle index 59e6d86d..8e60e1ef 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { } } -def base_version = '0.4-SNAPSHOT' +def base_version = '0.5-SNAPSHOT' allprojects { group 'dev.rudiments' @@ -43,11 +43,6 @@ configure(scalaModules()) { 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' diff --git a/file/build.gradle b/codec/build.gradle similarity index 72% rename from file/build.gradle rename to codec/build.gradle index 0ed26752..a3913a14 100644 --- a/file/build.gradle +++ b/codec/build.gradle @@ -1,7 +1,4 @@ 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' 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/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/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/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/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 deleted file mode 100644 index 5ed0fecc..00000000 --- a/core/src/main/scala/dev/rudiments/hardcore/TypeSystem.scala +++ /dev/null @@ -1,76 +0,0 @@ -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 - } - } - 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) - - 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 - } - - @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 - } - } - - val typeSystem: Map[Location, Predicate] = seal() -} 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/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/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/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/example/build.gradle b/example/build.gradle index 01a00ab9..f8f243a4 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -1,19 +1,8 @@ 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 project(':codec') implementation 'ch.qos.logback:logback-classic:1.2.7' - - testImplementation 'com.typesafe.akka:akka-testkit_2.13:2.6.17' - testImplementation 'com.typesafe.akka:akka-http-testkit_2.13:10.2.7' } task example(type: JavaExec, dependsOn: classes) { 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 deleted file mode 100644 index 47d97b58..00000000 --- a/example/src/main/scala/dev/rudiments/app/Main.scala +++ /dev/null @@ -1,39 +0,0 @@ -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 - -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") - } - } -} 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/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/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/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/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..272e48dc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,7 +1,4 @@ rootProject.name = 'hardcore' include 'core' -include 'http' -include 'file' -include 'sql' -include 'management' +include 'codec' include 'example' 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 From 7ca00168e92918165c2abe28ae16cb6370e2b94a Mon Sep 17 00:00:00 2001 From: gennady Date: Sun, 5 Mar 2023 19:49:18 +0600 Subject: [PATCH 02/75] upgrade to scala 3, new draft --- build.gradle | 11 ++- codec/build.gradle | 6 +- .../scala/dev/rudiments/hardcore/CRUD.scala | 35 ++++++++ .../dev/rudiments/hardcore/Location.scala | 29 ++++++ .../dev/rudiments/hardcore/Message.scala | 11 +++ .../scala/dev/rudiments/hardcore/Node.scala | 85 ++++++++++++++++++ .../scala/dev/rudiments/hardcore/Root.scala | 7 ++ .../scala/dev/rudiments/hardcore/Thing.scala | 26 ++++++ .../main/scala/dev/rudiments/logs/Log.scala | 7 ++ .../scala/test/dev/rudiments/CheckTest.scala | 14 +++ .../dev/rudiments/hardcore/NodeSpec.scala | 16 ++++ example/build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 59536 -> 59821 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- 14 files changed, 240 insertions(+), 11 deletions(-) create mode 100644 core/src/main/scala/dev/rudiments/hardcore/CRUD.scala create mode 100644 core/src/main/scala/dev/rudiments/hardcore/Location.scala create mode 100644 core/src/main/scala/dev/rudiments/hardcore/Message.scala create mode 100644 core/src/main/scala/dev/rudiments/hardcore/Node.scala create mode 100644 core/src/main/scala/dev/rudiments/hardcore/Root.scala create mode 100644 core/src/main/scala/dev/rudiments/hardcore/Thing.scala create mode 100644 core/src/main/scala/dev/rudiments/logs/Log.scala create mode 100644 core/src/test/scala/test/dev/rudiments/CheckTest.scala create mode 100644 core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala diff --git a/build.gradle b/build.gradle index 8e60e1ef..b05bc92c 100644 --- a/build.gradle +++ b/build.gradle @@ -42,15 +42,14 @@ configure(scalaModules()) { } dependencies { - implementation 'org.scala-lang:scala-library:2.13.7' + implementation 'org.scala-lang:scala3-library_3:3.1.3' - implementation 'com.typesafe.scala-logging:scala-logging_2.13:3.9.4' - implementation 'org.slf4j:slf4j-api:1.7.32' + implementation 'org.slf4j:slf4j-api:2.0.6' - testImplementation 'org.scalatest:scalatest_2.13:3.2.10' - testImplementation 'org.scalatestplus:junit-4-13_2.13:3.2.10.0' + testImplementation 'org.scalatest:scalatest_3:3.2.15' + testImplementation 'org.scalatestplus:junit-4-13_3:3.2.15.0' testImplementation 'junit:junit:4.13.2' - testImplementation 'ch.qos.logback:logback-classic:1.2.7' + testImplementation 'ch.qos.logback:logback-classic:1.4.5' } } \ No newline at end of file diff --git a/codec/build.gradle b/codec/build.gradle index a3913a14..880dc354 100644 --- a/codec/build.gradle +++ b/codec/build.gradle @@ -1,5 +1,5 @@ dependencies { - 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 'io.circe:circe-core_3:0.14.5' + implementation 'io.circe:circe-generic_3:0.14.5' + implementation 'io.circe:circe-generic-extras_3:0.14.5' } \ No newline at end of file diff --git a/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala b/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala new file mode 100644 index 00000000..1e055694 --- /dev/null +++ b/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala @@ -0,0 +1,35 @@ +package dev.rudiments.hardcore + +import scala.collection.immutable.ListMap + +sealed trait CRUD {} +object CRUD {} + +sealed trait Cmd extends Command with CRUD +sealed trait Qry extends Query with CRUD +sealed trait Evt extends Event with CRUD +sealed trait Rep extends Report with CRUD +sealed trait Err extends Error with CRUD + +case class Create(value: Any) extends Cmd +case class Read(where: Location) extends Qry +case class Update(old: Any, value: Any) extends Cmd +case class Delete(old: Any) extends Cmd + +case class Created(value: Any) extends Evt +case class Readen(value: Any) extends Rep +case class Updated(old: Any, value: Any) extends Evt +case class Deleted(old: Any) extends Evt +case class Commit(events: (Location, Evt)*) extends Evt { + def cud: Seq[(Location, Evt)] = events.foldLeft(Seq.empty[(Location, Evt)]) { + case (m, (prefix, c: Commit)) => m ++ c.cud.map { case (l, evt) => prefix / l -> evt } + case (m, p) => m :+ p //Created, Updated, Deleted + } + //TODO reject commit with intersecting locations inside? +} +case class Applied(commit: Commit) extends Evt + +case class NotFound(id: Location) extends Rep +case class Conflict(incoming: Event, actual: Out) extends Err +case class NotSupported(in: In) extends Err +case class MultiError(errors: (Location, Out)*) extends Err 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..556514da --- /dev/null +++ b/core/src/main/scala/dev/rudiments/hardcore/Location.scala @@ -0,0 +1,29 @@ +package dev.rudiments.hardcore + +sealed trait Location extends Product { + def /(l: Location): Location +} + +case object Self extends Location { + def /(l: Location): Location = l match { + case Self => Self + case id: ID => id + case path: Path => path + } +} + +final case class ID(key: Any) extends Location { + def /(l: Location): Location = l match { + case Self => this // or Path(id, Self)? + case id: ID => Path(this, id) + case path: Path => Path(this +: path.ids: _*) + } +} + +final case class Path(ids: ID*) extends Location { + def /(l: Location): Location = l match { + case Self => this // or Path(ids :+ Self)? + case id: ID => Path(ids :+ id: _*) + case path: Path => Path(ids ++ path.ids: _*) + } +} diff --git a/core/src/main/scala/dev/rudiments/hardcore/Message.scala b/core/src/main/scala/dev/rudiments/hardcore/Message.scala new file mode 100644 index 00000000..8e08d66b --- /dev/null +++ b/core/src/main/scala/dev/rudiments/hardcore/Message.scala @@ -0,0 +1,11 @@ +package dev.rudiments.hardcore + +sealed trait Message extends Product +sealed trait In extends Message +sealed trait Out extends Message + +trait Command extends In +trait Query extends In +trait Event extends Out +trait Report extends Out +trait Error extends Out \ 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 new file mode 100644 index 00000000..d1afb70c --- /dev/null +++ b/core/src/main/scala/dev/rudiments/hardcore/Node.scala @@ -0,0 +1,85 @@ +package dev.rudiments.hardcore + +import java.lang +import scala.collection.immutable.ListMap +import scala.collection.mutable + +case class Node( + state: mutable.Map[Location, Any] = mutable.Map.empty +) { + def read(key: Location): Out with CRUD = { + key match { + case Self | ID(_) => state.get(key) match { + case Some(v) => Readen(v) + case None => NotFound(key) + } + case p: Path => NotSupported(Read(p)) + } + } + + def apply(key: Location, event: Evt): Out with CRUD = { + readChain(key, event) match { + case evt: Evt => unsafeApply(key, evt) + case other => other + } + } + + def unsafeApply(key: Location, event: Evt): Evt = event match { + case c@Created(v) => state += (key -> v); c + case u@Updated(_, v) => state += (key -> v); u + case d: Deleted => state -= key; d + case other => throw new IllegalArgumentException(s"Not event: $other") + } + + def apply(commit: Commit): Out = { // TODO merge with apply(location, event) + val (errors, events) = commit.cud + .map { case (l, evt) => l -> readChain(l, evt) } + .partitionMap { + case (l, evt: Evt) => Right(l -> evt) + case p => Left(p) + } + + if (errors.isEmpty) { + val executed = events.map { case (l, evt) => l -> this.unsafeApply(l, evt) } + Applied(Commit(executed:_*)) + } else { + MultiError(errors:_*) + } + } + + def readChain(l: Location, after: Evt): Out with CRUD = (read(l), after) match { + case (NotFound(_), c: Created) => c + case (Readen(v), u@Updated(v1, v2)) if v == v1 && v1 != v2 => u + case (r@Readen(v), Updated(v1, v2)) if v == v1 && v1 == v2 => r + case (Readen(v), d@Deleted(old)) if v == old => d + //TODO commit + case (actual, event) => Conflict(event, actual) + } + + def chain(before: Out with CRUD, after: Evt): Out with CRUD = (before, after) match { + case (NotFound(_), c: Created) => c + case (Deleted(_), c: Created) => c + case (Readen(v), u@Updated(v1, v2)) if v == v1 && v1 != v2 => u + case (Created(v), u@Updated(v1, v2)) if v == v1 && v1 != v2 => u + case (Updated(_, v), u@Updated(v1, v2)) if v == v1 && v1 != v2 => u + case (Readen(v), d@Deleted(old)) if v == old => d + case (Created(v), d@Deleted(old)) if v == old => d + case (Updated(_, v), d@Deleted(old)) if v == old => d + //TODO commit + case (actual, event) => Conflict(event, actual) + } +} + +object Node { + val empty: Node = Node(mutable.Map.empty) + + def from(c: Commit): Either[MultiError, Node] = { + val node = Node.empty + node.apply(c) match { + case _: Applied => Right(node) + case m: MultiError => Left(m) + case other => + throw new IllegalArgumentException(s"Should never happen: $other") + } + } +} \ No newline at end of file diff --git a/core/src/main/scala/dev/rudiments/hardcore/Root.scala b/core/src/main/scala/dev/rudiments/hardcore/Root.scala new file mode 100644 index 00000000..627a251c --- /dev/null +++ b/core/src/main/scala/dev/rudiments/hardcore/Root.scala @@ -0,0 +1,7 @@ +package dev.rudiments.hardcore + +object Root { + private val node = Node.empty + + +} diff --git a/core/src/main/scala/dev/rudiments/hardcore/Thing.scala b/core/src/main/scala/dev/rudiments/hardcore/Thing.scala new file mode 100644 index 00000000..cecd6967 --- /dev/null +++ b/core/src/main/scala/dev/rudiments/hardcore/Thing.scala @@ -0,0 +1,26 @@ +package dev.rudiments.hardcore + +sealed trait Thing extends Product {} + +sealed trait Predicate extends Thing {} + +final case class Type( + fields: (ID, Field)* +) extends Predicate { + def data(values: Any*): Data = Data(this, values) +} + +final case class Field( + spec: Predicate, + kind: FieldKind = Required +) extends Thing + +sealed trait FieldKind {} +case object Required extends FieldKind +case class WithDefault(d: Any) extends FieldKind +object Optional extends WithDefault(None) + +final case class Data( + what: Predicate, + data: Any +) extends Thing diff --git a/core/src/main/scala/dev/rudiments/logs/Log.scala b/core/src/main/scala/dev/rudiments/logs/Log.scala new file mode 100644 index 00000000..c659d342 --- /dev/null +++ b/core/src/main/scala/dev/rudiments/logs/Log.scala @@ -0,0 +1,7 @@ +package dev.rudiments.logs + +import org.slf4j.{Logger, LoggerFactory} + +trait Log { + lazy val log: Logger = LoggerFactory.getLogger(this.getClass) +} diff --git a/core/src/test/scala/test/dev/rudiments/CheckTest.scala b/core/src/test/scala/test/dev/rudiments/CheckTest.scala new file mode 100644 index 00000000..c2f41b46 --- /dev/null +++ b/core/src/test/scala/test/dev/rudiments/CheckTest.scala @@ -0,0 +1,14 @@ +package test.dev.rudiments + +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 CheckTest extends AnyWordSpec with Matchers { + "always true" in { + val a = true + a should be(true) + } +} diff --git a/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala b/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala new file mode 100644 index 00000000..f0aba264 --- /dev/null +++ b/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala @@ -0,0 +1,16 @@ +package test.dev.rudiments.hardcore + +import dev.rudiments.hardcore.Node +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 { + "Node" should { + "created empty" in { + Node.empty.state.size should be (0) + } + } +} diff --git a/example/build.gradle b/example/build.gradle index f8f243a4..e9ea93db 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -2,7 +2,7 @@ dependencies { implementation project(':core') implementation project(':codec') - implementation 'ch.qos.logback:logback-classic:1.2.7' + implementation 'ch.qos.logback:logback-classic:1.4.5' } task example(type: JavaExec, dependsOn: classes) { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f2ae8848c63b8b4dea2cb829da983f2fa..41d9927a4d4fb3f96a785543079b8df6723c946b 100644 GIT binary patch delta 8958 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2e6e5897..e1bef7e8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ 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.0.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From bcc9f3c66a4b783a71eb22e9ab3c52ee2a11d0d3 Mon Sep 17 00:00:00 2001 From: gennady Date: Mon, 6 Mar 2023 02:09:36 +0600 Subject: [PATCH 03/75] drop intermediate traits with CRUD, make path in node --- .../scala/dev/rudiments/hardcore/CRUD.scala | 36 ++++++++----------- .../dev/rudiments/hardcore/Location.scala | 13 +++++++ .../scala/dev/rudiments/hardcore/Node.scala | 27 ++++++++------ .../scala/dev/rudiments/hardcore/Thing.scala | 7 ++++ .../dev/rudiments/hardcore/NodeSpec.scala | 2 +- 5 files changed, 53 insertions(+), 32 deletions(-) diff --git a/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala b/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala index 1e055694..48d6f627 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala @@ -5,31 +5,25 @@ import scala.collection.immutable.ListMap sealed trait CRUD {} object CRUD {} -sealed trait Cmd extends Command with CRUD -sealed trait Qry extends Query with CRUD -sealed trait Evt extends Event with CRUD -sealed trait Rep extends Report with CRUD -sealed trait Err extends Error with CRUD +case class Create(value: Product) extends Command with CRUD +case class Read(where: Location) extends Query with CRUD +case class Update(old: Product, value: Product) extends Command with CRUD +case class Delete(old: Product) extends Command with CRUD -case class Create(value: Any) extends Cmd -case class Read(where: Location) extends Qry -case class Update(old: Any, value: Any) extends Cmd -case class Delete(old: Any) extends Cmd - -case class Created(value: Any) extends Evt -case class Readen(value: Any) extends Rep -case class Updated(old: Any, value: Any) extends Evt -case class Deleted(old: Any) extends Evt -case class Commit(events: (Location, Evt)*) extends Evt { - def cud: Seq[(Location, Evt)] = events.foldLeft(Seq.empty[(Location, Evt)]) { +case class Created(value: Product) extends Event with CRUD +case class Readen(value: Product) extends Report with CRUD +case class Updated(old: Product, value: Product) extends Event with CRUD +case class Deleted(old: Product) extends Event with CRUD +case class Commit(events: (Location, Event with CRUD)*) extends Event with CRUD { + def cud: Seq[(Location, Event with CRUD)] = events.foldLeft(Seq.empty[(Location, Event with CRUD)]) { case (m, (prefix, c: Commit)) => m ++ c.cud.map { case (l, evt) => prefix / l -> evt } case (m, p) => m :+ p //Created, Updated, Deleted } //TODO reject commit with intersecting locations inside? } -case class Applied(commit: Commit) extends Evt +case class Applied(commit: Commit) extends Event with CRUD -case class NotFound(id: Location) extends Rep -case class Conflict(incoming: Event, actual: Out) extends Err -case class NotSupported(in: In) extends Err -case class MultiError(errors: (Location, Out)*) extends Err +case class NotFound(id: Location) extends Report with CRUD +case class Conflict(incoming: Event, actual: Out) extends Error with CRUD +case class NotSupported(in: In) extends Error with CRUD +case class MultiError(errors: (Location, Out)*) extends Error with CRUD diff --git a/core/src/main/scala/dev/rudiments/hardcore/Location.scala b/core/src/main/scala/dev/rudiments/hardcore/Location.scala index 556514da..ee926c7d 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Location.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Location.scala @@ -1,5 +1,7 @@ package dev.rudiments.hardcore +import java.lang + sealed trait Location extends Product { def /(l: Location): Location } @@ -21,9 +23,20 @@ final case class ID(key: Any) extends Location { } final case class Path(ids: ID*) extends Location { + if(ids.size <= 2) + throw new IllegalArgumentException(s"Path should be at least with 2 IDs, but got: ${ids.size}") + def /(l: Location): Location = l match { case Self => this // or Path(ids :+ Self)? case id: ID => Path(ids :+ id: _*) case path: Path => Path(ids ++ path.ids: _*) } + + def head: ID = ids.head + def tail: Location = + if (ids.size == 1) { + ids.last + } else { + Path(ids.tail :_*) + } } diff --git a/core/src/main/scala/dev/rudiments/hardcore/Node.scala b/core/src/main/scala/dev/rudiments/hardcore/Node.scala index d1afb70c..52cd0212 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Node.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Node.scala @@ -4,8 +4,8 @@ import java.lang import scala.collection.immutable.ListMap import scala.collection.mutable -case class Node( - state: mutable.Map[Location, Any] = mutable.Map.empty +class Node( + state: mutable.Map[Location, Product] = mutable.Map.empty ) { def read(key: Location): Out with CRUD = { key match { @@ -13,18 +13,23 @@ case class Node( case Some(v) => Readen(v) case None => NotFound(key) } - case p: Path => NotSupported(Read(p)) + case p: Path => + state.get(p.head) match { + case Some(node: Node) => node.read(p.tail) //TODO optimize if needed + case Some(_) => NotSupported(Read(p.tail)) + case None => NotFound(p) + } } } - def apply(key: Location, event: Evt): Out with CRUD = { + def apply(key: Location, event: Event with CRUD): Out with CRUD = { readChain(key, event) match { - case evt: Evt => unsafeApply(key, evt) + case evt: Event with CRUD => unsafeApply(key, evt) case other => other } } - def unsafeApply(key: Location, event: Evt): Evt = event match { + def unsafeApply(key: Location, event: Event with CRUD): Event with CRUD = event match { case c@Created(v) => state += (key -> v); c case u@Updated(_, v) => state += (key -> v); u case d: Deleted => state -= key; d @@ -35,7 +40,7 @@ case class Node( val (errors, events) = commit.cud .map { case (l, evt) => l -> readChain(l, evt) } .partitionMap { - case (l, evt: Evt) => Right(l -> evt) + case (l, evt: Event with CRUD) => Right(l -> evt) case p => Left(p) } @@ -47,7 +52,7 @@ case class Node( } } - def readChain(l: Location, after: Evt): Out with CRUD = (read(l), after) match { + def readChain(l: Location, after: Event with CRUD): Out with CRUD = (read(l), after) match { case (NotFound(_), c: Created) => c case (Readen(v), u@Updated(v1, v2)) if v == v1 && v1 != v2 => u case (r@Readen(v), Updated(v1, v2)) if v == v1 && v1 == v2 => r @@ -56,7 +61,7 @@ case class Node( case (actual, event) => Conflict(event, actual) } - def chain(before: Out with CRUD, after: Evt): Out with CRUD = (before, after) match { + def chain(before: Out with CRUD, after: Event with CRUD): Out with CRUD = (before, after) match { case (NotFound(_), c: Created) => c case (Deleted(_), c: Created) => c case (Readen(v), u@Updated(v1, v2)) if v == v1 && v1 != v2 => u @@ -68,10 +73,12 @@ case class Node( //TODO commit case (actual, event) => Conflict(event, actual) } + + def size: Int = this.state.size } object Node { - val empty: Node = Node(mutable.Map.empty) + def empty: Node = new Node() def from(c: Commit): Either[MultiError, Node] = { val node = Node.empty diff --git a/core/src/main/scala/dev/rudiments/hardcore/Thing.scala b/core/src/main/scala/dev/rudiments/hardcore/Thing.scala index cecd6967..17f2c2e3 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Thing.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Thing.scala @@ -9,6 +9,11 @@ final case class Type( ) extends Predicate { def data(values: Any*): Data = Data(this, values) } +object Type { + def of(predicates: (ID, Predicate)*): Type = new Type( + predicates.map((id, p) => id -> Field(p, Required)) :_* + ) +} final case class Field( spec: Predicate, @@ -24,3 +29,5 @@ final case class Data( what: Predicate, data: Any ) extends Thing + +case object Nothing extends Predicate {} diff --git a/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala b/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala index f0aba264..3b2944b2 100644 --- a/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala +++ b/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala @@ -10,7 +10,7 @@ import org.scalatestplus.junit.JUnitRunner class NodeSpec extends AnyWordSpec with Matchers { "Node" should { "created empty" in { - Node.empty.state.size should be (0) + Node.empty.size should be (0) } } } From b2ae3e37d4ffbc028157aa2db02e74f0f7628b6e Mon Sep 17 00:00:00 2001 From: gennady Date: Mon, 6 Mar 2023 03:20:48 +0600 Subject: [PATCH 04/75] improve reading paths, some dsl --- .../scala/dev/rudiments/hardcore/Node.scala | 64 +++++++++++-------- .../dev/rudiments/hardcore/NodeSpec.scala | 24 ++++++- 2 files changed, 60 insertions(+), 28 deletions(-) diff --git a/core/src/main/scala/dev/rudiments/hardcore/Node.scala b/core/src/main/scala/dev/rudiments/hardcore/Node.scala index 52cd0212..f9327132 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Node.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Node.scala @@ -4,7 +4,7 @@ import java.lang import scala.collection.immutable.ListMap import scala.collection.mutable -class Node( +case class Node( state: mutable.Map[Location, Product] = mutable.Map.empty ) { def read(key: Location): Out with CRUD = { @@ -22,10 +22,23 @@ class Node( } } + def tread(l: Location): (Node, Location, Out with CRUD) = l match + case Self | ID(_) => state.get(l) match + case Some(node: Node) => (node, Self, Readen(node)) + case Some(v) => (this, l, Readen(v)) + case None => (this, l, NotFound(l)) + case p: Path => state.get(p.head) match + case Some(node: Node) => + val readen = node.tread(p.tail) + (readen._1, p.head / readen._2, readen._3) + case Some(_) => (this, l, NotSupported(Read(p.tail))) + case None => (this, l, NotFound(p)) + + def apply(key: Location, event: Event with CRUD): Out with CRUD = { readChain(key, event) match { - case evt: Event with CRUD => unsafeApply(key, evt) - case other => other + case (node, evt: Event with CRUD) => node.unsafeApply(key, evt) + case (_, other) => other } } @@ -40,41 +53,40 @@ class Node( val (errors, events) = commit.cud .map { case (l, evt) => l -> readChain(l, evt) } .partitionMap { - case (l, evt: Event with CRUD) => Right(l -> evt) - case p => Left(p) + case (l, (node, evt: Event with CRUD)) => Right((node, l, evt)) + case (l, (_, o)) => Left(l -> o) } if (errors.isEmpty) { - val executed = events.map { case (l, evt) => l -> this.unsafeApply(l, evt) } + val executed = events.map { case (node, l, evt) => l -> node.unsafeApply(l, evt) } Applied(Commit(executed:_*)) } else { MultiError(errors:_*) } } - def readChain(l: Location, after: Event with CRUD): Out with CRUD = (read(l), after) match { - case (NotFound(_), c: Created) => c - case (Readen(v), u@Updated(v1, v2)) if v == v1 && v1 != v2 => u - case (r@Readen(v), Updated(v1, v2)) if v == v1 && v1 == v2 => r - case (Readen(v), d@Deleted(old)) if v == old => d - //TODO commit - case (actual, event) => Conflict(event, actual) - } - - def chain(before: Out with CRUD, after: Event with CRUD): Out with CRUD = (before, after) match { - case (NotFound(_), c: Created) => c - case (Deleted(_), c: Created) => c - case (Readen(v), u@Updated(v1, v2)) if v == v1 && v1 != v2 => u - case (Created(v), u@Updated(v1, v2)) if v == v1 && v1 != v2 => u - case (Updated(_, v), u@Updated(v1, v2)) if v == v1 && v1 != v2 => u - case (Readen(v), d@Deleted(old)) if v == old => d - case (Created(v), d@Deleted(old)) if v == old => d - case (Updated(_, v), d@Deleted(old)) if v == old => d - //TODO commit - case (actual, event) => Conflict(event, actual) + def readChain( + l: Location, after: Event with CRUD + ): (Node, Out with CRUD) = (tread(l), after) match { + case ((n, _, NotFound(_)), c: Created) => n -> c + case ((n, _, Readen(v)), u@Updated(v1, v2)) if v == v1 && v1 != v2 => n -> u + case ((n, _, r@Readen(v)), Updated(v1, v2)) if v == v1 && v1 == v2 => n -> r + case ((n, _, Readen(v)), d@Deleted(old)) if v == old => n -> d + case ((_, _, Readen(node: Node)), c: Commit) => node -> c + case ((n, _, actual), event) => n -> Conflict(event, actual) } def size: Int = this.state.size + + def >+ (pair: (Location, Product)): Out with CRUD = this.apply(pair._1, Created(pair._2)) + def >* (pair: (Location, Product)): Out with CRUD = this.read(pair._1) match { + case Readen(r) => this.apply(pair._1, Updated(r, pair._2)) + case other => other + } + def >- (l: Location): Out with CRUD = this.read(l) match { + case Readen(r) => this.apply(l, Deleted(r)) + case other => other + } } object Node { diff --git a/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala b/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala index 3b2944b2..598dd4cb 100644 --- a/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala +++ b/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala @@ -1,6 +1,6 @@ package test.dev.rudiments.hardcore -import dev.rudiments.hardcore.Node +import dev.rudiments.hardcore._ import org.junit.runner.RunWith import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -8,9 +8,29 @@ import org.scalatestplus.junit.JUnitRunner @RunWith(classOf[JUnitRunner]) class NodeSpec extends AnyWordSpec with Matchers { + case class Something(a: String, i: Int) + val s1 = Something("abc", 42) + val s2 = Something("cde", 24) + "Node" should { + val node: Node = Node.empty "created empty" in { - Node.empty.size should be (0) + node.size should be (0) + } + + "can add something" in { + node >+ ID("42") -> s1 should be (Created(s1)) + node.size should be (1) + } + + "can update something" in { + node >* ID("42") -> s2 should be(Updated(s1, s2)) + node.size should be(1) + } + + "can delete something" in { + node >- ID("42") should be(Deleted(s2)) + node.size should be(0) } } } From c00777dd1082685642dae637ee49d05d48e2739b Mon Sep 17 00:00:00 2001 From: gennady Date: Mon, 6 Mar 2023 04:20:44 +0600 Subject: [PATCH 05/75] error with nested paths insertion --- .../dev/rudiments/hardcore/Location.scala | 4 +-- .../scala/dev/rudiments/hardcore/Node.scala | 4 ++- .../dev/rudiments/hardcore/NodeSpec.scala | 28 +++++++++++++++---- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/core/src/main/scala/dev/rudiments/hardcore/Location.scala b/core/src/main/scala/dev/rudiments/hardcore/Location.scala index ee926c7d..66626bf9 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Location.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Location.scala @@ -23,7 +23,7 @@ final case class ID(key: Any) extends Location { } final case class Path(ids: ID*) extends Location { - if(ids.size <= 2) + if(ids.size < 2) throw new IllegalArgumentException(s"Path should be at least with 2 IDs, but got: ${ids.size}") def /(l: Location): Location = l match { @@ -34,7 +34,7 @@ final case class Path(ids: ID*) extends Location { def head: ID = ids.head def tail: Location = - if (ids.size == 1) { + if (ids.size == 2) { ids.last } else { Path(ids.tail :_*) diff --git a/core/src/main/scala/dev/rudiments/hardcore/Node.scala b/core/src/main/scala/dev/rudiments/hardcore/Node.scala index f9327132..088982a1 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Node.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Node.scala @@ -49,7 +49,7 @@ case class Node( case other => throw new IllegalArgumentException(s"Not event: $other") } - def apply(commit: Commit): Out = { // TODO merge with apply(location, event) + def apply(commit: Commit): Out with CRUD = { // TODO merge with apply(location, event) val (errors, events) = commit.cud .map { case (l, evt) => l -> readChain(l, evt) } .partitionMap { @@ -87,6 +87,8 @@ case class Node( case Readen(r) => this.apply(l, Deleted(r)) case other => other } + + def >> (pairs: (Location, Event with CRUD)*): Out with CRUD = this.apply(Commit(pairs:_*)) } object Node { diff --git a/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala b/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala index 598dd4cb..21808f72 100644 --- a/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala +++ b/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala @@ -9,8 +9,8 @@ import org.scalatestplus.junit.JUnitRunner @RunWith(classOf[JUnitRunner]) class NodeSpec extends AnyWordSpec with Matchers { case class Something(a: String, i: Int) - val s1 = Something("abc", 42) - val s2 = Something("cde", 24) + private val s1 = Something("abc", 42) + private val s2 = Something("cde", 24) "Node" should { val node: Node = Node.empty @@ -18,19 +18,37 @@ class NodeSpec extends AnyWordSpec with Matchers { node.size should be (0) } - "can add something" in { + "add something" in { node >+ ID("42") -> s1 should be (Created(s1)) node.size should be (1) } - "can update something" in { + "update something" in { node >* ID("42") -> s2 should be(Updated(s1, s2)) node.size should be(1) } - "can delete something" in { + "delete something" in { node >- ID("42") should be(Deleted(s2)) node.size should be(0) } + + "apply commit" in { + val pairs = (1 to 10).map (i => ID(i.toString) -> Created(Something(i.toHexString, i))) + node(Commit(pairs:_*)) should be(Applied(Commit(pairs:_*))) + node.size should be(10) + } + + "create nested node" in { + node >+ ID("n") -> Node.empty should be (Created(Node.empty)) + node.size should be(11) + } + + "put nested values" in { + val p = ID("n") / ID("123") + node >+ p -> s1 should be (Created(s1)) + node.size should be (11) + node.read(p) should be (Readen(s1)) + } } } From f06b6f7e4f289f1fd4cc338e049b6b1506378a29 Mon Sep 17 00:00:00 2001 From: gennady Date: Tue, 7 Mar 2023 05:07:49 +0600 Subject: [PATCH 06/75] use cursor instead of complex tuples --- .../dev/rudiments/hardcore/Location.scala | 10 ++ .../scala/dev/rudiments/hardcore/Node.scala | 134 +++++++++++------- 2 files changed, 90 insertions(+), 54 deletions(-) diff --git a/core/src/main/scala/dev/rudiments/hardcore/Location.scala b/core/src/main/scala/dev/rudiments/hardcore/Location.scala index 66626bf9..507f5427 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Location.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Location.scala @@ -4,6 +4,16 @@ import java.lang sealed trait Location extends Product { def /(l: Location): Location + final def toIds: Seq[ID] = this match + case Self => Seq.empty + case id: ID => id :: Nil + case path: Path => path.ids +} +object Location { + def apply(ids: ID*): Location = ids match + case Nil => Self + case h :: Nil => h + case _ => Path(ids:_*) } case object Self extends Location { diff --git a/core/src/main/scala/dev/rudiments/hardcore/Node.scala b/core/src/main/scala/dev/rudiments/hardcore/Node.scala index 088982a1..2b8dc701 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Node.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Node.scala @@ -1,81 +1,42 @@ package dev.rudiments.hardcore +import dev.rudiments.hardcore.Node.Cursor + import java.lang +import scala.annotation.tailrec import scala.collection.immutable.ListMap import scala.collection.mutable case class Node( state: mutable.Map[Location, Product] = mutable.Map.empty ) { - def read(key: Location): Out with CRUD = { - key match { - case Self | ID(_) => state.get(key) match { - case Some(v) => Readen(v) - case None => NotFound(key) - } - case p: Path => - state.get(p.head) match { - case Some(node: Node) => node.read(p.tail) //TODO optimize if needed - case Some(_) => NotSupported(Read(p.tail)) - case None => NotFound(p) - } - } - } + private def cursor() = new Node.Cursor(this) - def tread(l: Location): (Node, Location, Out with CRUD) = l match - case Self | ID(_) => state.get(l) match - case Some(node: Node) => (node, Self, Readen(node)) - case Some(v) => (this, l, Readen(v)) - case None => (this, l, NotFound(l)) - case p: Path => state.get(p.head) match - case Some(node: Node) => - val readen = node.tread(p.tail) - (readen._1, p.head / readen._2, readen._3) - case Some(_) => (this, l, NotSupported(Read(p.tail))) - case None => (this, l, NotFound(p)) - - - def apply(key: Location, event: Event with CRUD): Out with CRUD = { - readChain(key, event) match { - case (node, evt: Event with CRUD) => node.unsafeApply(key, evt) - case (_, other) => other - } - } + def read(l: Location): Out with CRUD = cursor().search(l).out - def unsafeApply(key: Location, event: Event with CRUD): Event with CRUD = event match { - case c@Created(v) => state += (key -> v); c - case u@Updated(_, v) => state += (key -> v); u - case d: Deleted => state -= key; d - case other => throw new IllegalArgumentException(s"Not event: $other") + def apply(l: Location, event: Event with CRUD): Out with CRUD = { + cursor() + .search(l) + .check(event) + .unsafeApply() } def apply(commit: Commit): Out with CRUD = { // TODO merge with apply(location, event) val (errors, events) = commit.cud - .map { case (l, evt) => l -> readChain(l, evt) } - .partitionMap { - case (l, (node, evt: Event with CRUD)) => Right((node, l, evt)) - case (l, (_, o)) => Left(l -> o) + .map { (l, evt) => l -> cursor().search(l).check(evt) } + .partitionMap { (l, c) => c.out match + case _: Event with CRUD => Right(l -> c) + case _ => Left(l -> c.out) } if (errors.isEmpty) { - val executed = events.map { case (node, l, evt) => l -> node.unsafeApply(l, evt) } + val executed = events.map { (l, cur) => l -> cur.unsafeApply() } Applied(Commit(executed:_*)) } else { MultiError(errors:_*) } } - def readChain( - l: Location, after: Event with CRUD - ): (Node, Out with CRUD) = (tread(l), after) match { - case ((n, _, NotFound(_)), c: Created) => n -> c - case ((n, _, Readen(v)), u@Updated(v1, v2)) if v == v1 && v1 != v2 => n -> u - case ((n, _, r@Readen(v)), Updated(v1, v2)) if v == v1 && v1 == v2 => n -> r - case ((n, _, Readen(v)), d@Deleted(old)) if v == old => n -> d - case ((_, _, Readen(node: Node)), c: Commit) => node -> c - case ((n, _, actual), event) => n -> Conflict(event, actual) - } - def size: Int = this.state.size def >+ (pair: (Location, Product)): Out with CRUD = this.apply(pair._1, Created(pair._2)) @@ -103,4 +64,69 @@ object Node { throw new IllegalArgumentException(s"Should never happen: $other") } } + + final private class Cursor(starting: Node) { + var node: Node = starting + var to: List[ID] = Nil + var in: List[ID] = Nil + var out: Out with CRUD = _ + + def search(l: Location): Cursor = { + searchIds(l.toIds.toList) + this + } + + @tailrec + private def searchIds(ids: List[ID]): Unit = ids match { + case Nil => node.state.get(Self) match + case Some(v) => out = Readen(v) + case None => out = NotFound(Self) + case id :: Nil => node.state.get(id) match + case Some(n: Node) => + node = n + to = id +: to + out = Readen(n) + case Some(v) => + in = id :: Nil + out = Readen(v) + case None => + in = id :: Nil + out = NotFound(id) + case h :: t => node.state.get(h) match + case Some(n: Node) => + node = n + to = h +: to + searchIds(t) + case Some(_) => + in = h :: Nil + out = NotSupported(Read(Location(t:_*))) + case None => + in = Nil + out = NotFound(Location(ids:_*)) + } + + def inLocation: Location = Location(in:_*) + + def location: Location = Location(to ++ in: _*) + + def check(event: Event with CRUD): Cursor = { + out = (out, event) match + case (NotFound(_), c: Created) => c + case (Readen(v), u@Updated(v1, v2)) if v == v1 && v1 != v2 => u + case (r@Readen(v), Updated(v1, v2)) if v == v1 && v1 == v2 => r + case (Readen(v), d@Deleted(old)) if v == old => d + case (Readen(node: Node), c: Commit) => ??? //TODO check, not apply here + case (actual, event) => Conflict(event, actual) + + this + } + + def unsafeApply(): Event with CRUD = { + out match + case c@Created(v) => node.state += (inLocation -> v); c + case u@Updated(_, v) => node.state += (inLocation -> v); u + case d: Deleted => node.state -= inLocation; d + case other => throw new IllegalArgumentException(s"Not an event: $other") // or ignore? + } + } } \ No newline at end of file From 67834b7ea09934659de170be45509b3cbc10ffb9 Mon Sep 17 00:00:00 2001 From: gennady Date: Tue, 7 Mar 2023 22:37:54 +0600 Subject: [PATCH 07/75] apply nested commits --- .../scala/dev/rudiments/hardcore/CRUD.scala | 13 ++++++-- .../dev/rudiments/hardcore/Location.scala | 2 ++ .../scala/dev/rudiments/hardcore/Node.scala | 33 ++++++++++++++++--- .../dev/rudiments/hardcore/NodeSpec.scala | 8 +++++ 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala b/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala index 48d6f627..4307ed58 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala @@ -18,8 +18,16 @@ case class Commit(events: (Location, Event with CRUD)*) extends Event with CRUD def cud: Seq[(Location, Event with CRUD)] = events.foldLeft(Seq.empty[(Location, Event with CRUD)]) { case (m, (prefix, c: Commit)) => m ++ c.cud.map { case (l, evt) => prefix / l -> evt } case (m, p) => m :+ p //Created, Updated, Deleted - } - //TODO reject commit with intersecting locations inside? + } //TODO reject commit with intersecting locations inside? + + override def toString: String = events.map { (l, evt) => evt match + case Created(v) => s"$l +$v" + case Updated(v1, v2) => s"$l *$v1|->$v2" + case Deleted(v) => s"$l -$v" + case c: Commit => s"$l: Commit($c)" + case Applied(c) => s"$l: Applied($c)" + case other => s"$l -> {$other}" + }.mkString(";") } case class Applied(commit: Commit) extends Event with CRUD @@ -27,3 +35,4 @@ case class NotFound(id: Location) extends Report with CRUD case class Conflict(incoming: Event, actual: Out) extends Error with CRUD case class NotSupported(in: In) extends Error with CRUD case class MultiError(errors: (Location, Out)*) extends Error with CRUD +case class InternalError(t: Throwable) extends Error with CRUD diff --git a/core/src/main/scala/dev/rudiments/hardcore/Location.scala b/core/src/main/scala/dev/rudiments/hardcore/Location.scala index 507f5427..545f452a 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Location.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Location.scala @@ -49,4 +49,6 @@ final case class Path(ids: ID*) extends Location { } else { Path(ids.tail :_*) } + + override def toString: String = ids.map(_.key.toString).mkString("/") } diff --git a/core/src/main/scala/dev/rudiments/hardcore/Node.scala b/core/src/main/scala/dev/rudiments/hardcore/Node.scala index 2b8dc701..f89df3f8 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Node.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Node.scala @@ -30,8 +30,18 @@ case class Node( } if (errors.isEmpty) { - val executed = events.map { (l, cur) => l -> cur.unsafeApply() } - Applied(Commit(executed:_*)) + try { + val executed = events + .map { (l, cur) => l -> cur.unsafeApply() } //TODO rollback if errors + .filter { _._2 match + case _: Event with CRUD => false + case _ => true + } + if(executed.isEmpty) Applied(commit) + else MultiError(executed:_*) + } catch { + case e: Exception => InternalError(e) + } } else { MultiError(errors:_*) } @@ -115,18 +125,31 @@ object Node { case (Readen(v), u@Updated(v1, v2)) if v == v1 && v1 != v2 => u case (r@Readen(v), Updated(v1, v2)) if v == v1 && v1 == v2 => r case (Readen(v), d@Deleted(old)) if v == old => d - case (Readen(node: Node), c: Commit) => ??? //TODO check, not apply here + case (Readen(_), c: Commit) => + val errors = c.cud + .map { (l, evt) => l -> node.cursor().search(l).check(evt).out } + .filter { _._2 match + case _: Event with CRUD => false + case _ => true + } + + if (errors.isEmpty) { + c + } else { + MultiError(errors: _*) + } case (actual, event) => Conflict(event, actual) this } - def unsafeApply(): Event with CRUD = { + def unsafeApply(): Out with CRUD = { out match case c@Created(v) => node.state += (inLocation -> v); c case u@Updated(_, v) => node.state += (inLocation -> v); u case d: Deleted => node.state -= inLocation; d - case other => throw new IllegalArgumentException(s"Not an event: $other") // or ignore? + case c: Commit => node.apply(c); c + case other => other } } } \ No newline at end of file diff --git a/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala b/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala index 21808f72..50b99f9c 100644 --- a/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala +++ b/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala @@ -50,5 +50,13 @@ class NodeSpec extends AnyWordSpec with Matchers { node.size should be (11) node.read(p) should be (Readen(s1)) } + + "apply nested commits" in { + val pairs = (24 to 42).map (i => ID(i.toString) -> Created(Something(i.toHexString, i))) + node(Commit(ID("n") -> Commit(pairs:_*))) should be (Applied( + Commit(ID("n") -> Commit(pairs:_*)) + )) + node.state(ID("n")).asInstanceOf[Node].size should be (20) + } } } From 1d4fdd25459292ae12677fc1aef8bdf1a1fec2f9 Mon Sep 17 00:00:00 2001 From: gennady Date: Wed, 8 Mar 2023 04:44:48 +0600 Subject: [PATCH 08/75] salted map draft --- .../dev/rudiments/{logs => utils}/Log.scala | 2 +- .../scala/dev/rudiments/utils/SaltedMap.scala | 58 +++++++++++++++++++ .../dev/rudiments/utils/SaltedMapTest.scala | 20 +++++++ 3 files changed, 79 insertions(+), 1 deletion(-) rename core/src/main/scala/dev/rudiments/{logs => utils}/Log.scala (81%) create mode 100644 core/src/main/scala/dev/rudiments/utils/SaltedMap.scala create mode 100644 core/src/test/scala/test/dev/rudiments/utils/SaltedMapTest.scala diff --git a/core/src/main/scala/dev/rudiments/logs/Log.scala b/core/src/main/scala/dev/rudiments/utils/Log.scala similarity index 81% rename from core/src/main/scala/dev/rudiments/logs/Log.scala rename to core/src/main/scala/dev/rudiments/utils/Log.scala index c659d342..418fb554 100644 --- a/core/src/main/scala/dev/rudiments/logs/Log.scala +++ b/core/src/main/scala/dev/rudiments/utils/Log.scala @@ -1,4 +1,4 @@ -package dev.rudiments.logs +package dev.rudiments.utils import org.slf4j.{Logger, LoggerFactory} diff --git a/core/src/main/scala/dev/rudiments/utils/SaltedMap.scala b/core/src/main/scala/dev/rudiments/utils/SaltedMap.scala new file mode 100644 index 00000000..9ee7dfcf --- /dev/null +++ b/core/src/main/scala/dev/rudiments/utils/SaltedMap.scala @@ -0,0 +1,58 @@ +package dev.rudiments.utils + +import dev.rudiments.hardcore.Location + +class SaltedMap[K, +V](values: Array[(K, V)]) extends Map[K, V]{ + val salt: Int = 0;//TODO + + override def removed(key: K): SaltedMap[K, V] = new SaltedMap(values.filterNot(_._2 == key)) + + override def updated[V1 >: V](key: K, value: V1): SaltedMap[K, V1] = ???/*this.get(key) match { + case Some(v) if v == value => this + case Some(_) => + new SaltedMap[K, V1]( + values.update(saltedHash(key), key -> value.asInstanceOf[V]).asInstanceOf[Array[(K, V1)]] + ) //TODO improve, prob combine with get + case None => new SaltedMap(values :+ key -> value) + }*/ + + override def get(key: K): Option[V] = { + val found = values(saltedHash(key)) + if(found._1 == key) Some(found._2) else None + } + + override def iterator: Iterator[(K, V)] = values.iterator + + def saltedHash(key: K): Int = (key.hashCode() + salt) % values.length +} + +object SaltedMap extends Log { + def empty[K, V]: SaltedMap[K, V] = new SaltedMap[K, V](Array.empty) + + def apply[K, V](pairs: (K, V)*): SaltedMap[K, V] = new SaltedMap[K, V](pairs.toArray) + + + def h[K](key: K, size: Int, salt: Int): Int = { + val hash = 31 * 7 + salt + (31 * hash + key.hashCode()) % size + } + + val maxInterations: Int = Int.MaxValue + def salty[K](keys: List[K]): Int = { + var i: Int = 1 + var fit: Boolean = false + val expected = keys.indices + while(i <= maxInterations && !fit) { + fit = keys.map(k => h(k, keys.size, i)) == expected + if(!fit && i % 10_000_000 == 0) { + log.info("Failed on {}", i) + } + i += 1 + } + if(!fit) { + -1 + } else { + i + } + } +} \ No newline at end of file diff --git a/core/src/test/scala/test/dev/rudiments/utils/SaltedMapTest.scala b/core/src/test/scala/test/dev/rudiments/utils/SaltedMapTest.scala new file mode 100644 index 00000000..34a0dc11 --- /dev/null +++ b/core/src/test/scala/test/dev/rudiments/utils/SaltedMapTest.scala @@ -0,0 +1,20 @@ +package test.dev.rudiments.utils + +import dev.rudiments.utils.SaltedMap +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 SaltedMapTest extends AnyWordSpec with Matchers { + "Salted hash" should { + "fit for ordered keys" in { + SaltedMap.salty(0 :: 1 :: 2 :: 3 :: Nil) should be (4) + } + + "fit for unordered keys" ignore { + SaltedMap.salty(1 :: 3 :: 0 :: 2 :: Nil) should be(5) + } + } +} From 1c4958c1667e18153b182875bf381db693820bed Mon Sep 17 00:00:00 2001 From: gennady Date: Wed, 15 Mar 2023 11:18:04 +0600 Subject: [PATCH 09/75] lil refactor --- .../main/scala/dev/rudiments/hardcore/CRUD.scala | 11 +++++++---- .../main/scala/dev/rudiments/hardcore/Node.scala | 13 ++++++------- .../test/dev/rudiments/hardcore/NodeSpec.scala | 6 ++---- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala b/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala index 4307ed58..6a490bc5 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala @@ -15,8 +15,13 @@ case class Readen(value: Product) extends Report with CRUD case class Updated(old: Product, value: Product) extends Event with CRUD case class Deleted(old: Product) extends Event with CRUD case class Commit(events: (Location, Event with CRUD)*) extends Event with CRUD { - def cud: Seq[(Location, Event with CRUD)] = events.foldLeft(Seq.empty[(Location, Event with CRUD)]) { - case (m, (prefix, c: Commit)) => m ++ c.cud.map { case (l, evt) => prefix / l -> evt } + private val indexed = events.toMap + if(indexed.size != events.size) { + throw new IllegalArgumentException("Only unique locations allowed") + } + + def flatten: Seq[(Location, Event with CRUD)] = events.foldLeft(Seq.empty[(Location, Event with CRUD)]) { + case (m, (prefix, c: Commit)) => m ++ c.flatten.map { case (l, evt) => prefix / l -> evt } case (m, p) => m :+ p //Created, Updated, Deleted } //TODO reject commit with intersecting locations inside? @@ -25,11 +30,9 @@ case class Commit(events: (Location, Event with CRUD)*) extends Event with CRUD case Updated(v1, v2) => s"$l *$v1|->$v2" case Deleted(v) => s"$l -$v" case c: Commit => s"$l: Commit($c)" - case Applied(c) => s"$l: Applied($c)" case other => s"$l -> {$other}" }.mkString(";") } -case class Applied(commit: Commit) extends Event with CRUD case class NotFound(id: Location) extends Report with CRUD case class Conflict(incoming: Event, actual: Out) extends Error with CRUD diff --git a/core/src/main/scala/dev/rudiments/hardcore/Node.scala b/core/src/main/scala/dev/rudiments/hardcore/Node.scala index f89df3f8..2bdd22f3 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Node.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Node.scala @@ -21,8 +21,8 @@ case class Node( .unsafeApply() } - def apply(commit: Commit): Out with CRUD = { // TODO merge with apply(location, event) - val (errors, events) = commit.cud + def apply(commit: Commit): Out with CRUD = { + val (errors, events) = commit.flatten .map { (l, evt) => l -> cursor().search(l).check(evt) } .partitionMap { (l, c) => c.out match case _: Event with CRUD => Right(l -> c) @@ -37,7 +37,7 @@ case class Node( case _: Event with CRUD => false case _ => true } - if(executed.isEmpty) Applied(commit) + if(executed.isEmpty) commit else MultiError(executed:_*) } catch { case e: Exception => InternalError(e) @@ -58,8 +58,6 @@ case class Node( case Readen(r) => this.apply(l, Deleted(r)) case other => other } - - def >> (pairs: (Location, Event with CRUD)*): Out with CRUD = this.apply(Commit(pairs:_*)) } object Node { @@ -68,7 +66,7 @@ object Node { def from(c: Commit): Either[MultiError, Node] = { val node = Node.empty node.apply(c) match { - case _: Applied => Right(node) + case _: Commit => Right(node) case m: MultiError => Left(m) case other => throw new IllegalArgumentException(s"Should never happen: $other") @@ -126,7 +124,7 @@ object Node { case (r@Readen(v), Updated(v1, v2)) if v == v1 && v1 == v2 => r case (Readen(v), d@Deleted(old)) if v == old => d case (Readen(_), c: Commit) => - val errors = c.cud + val errors = c.flatten .map { (l, evt) => l -> node.cursor().search(l).check(evt).out } .filter { _._2 match case _: Event with CRUD => false @@ -138,6 +136,7 @@ object Node { } else { MultiError(errors: _*) } + case (nf: NotFound, _) => nf case (actual, event) => Conflict(event, actual) this diff --git a/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala b/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala index 50b99f9c..c3471bf8 100644 --- a/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala +++ b/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala @@ -35,7 +35,7 @@ class NodeSpec extends AnyWordSpec with Matchers { "apply commit" in { val pairs = (1 to 10).map (i => ID(i.toString) -> Created(Something(i.toHexString, i))) - node(Commit(pairs:_*)) should be(Applied(Commit(pairs:_*))) + node(Commit(pairs:_*)) should be(Commit(pairs:_*)) node.size should be(10) } @@ -53,9 +53,7 @@ class NodeSpec extends AnyWordSpec with Matchers { "apply nested commits" in { val pairs = (24 to 42).map (i => ID(i.toString) -> Created(Something(i.toHexString, i))) - node(Commit(ID("n") -> Commit(pairs:_*))) should be (Applied( - Commit(ID("n") -> Commit(pairs:_*)) - )) + node(Commit(ID("n") -> Commit(pairs:_*))) should be (Commit(ID("n") -> Commit(pairs:_*))) node.state(ID("n")).asInstanceOf[Node].size should be (20) } } From 3495b62e3ee4a435df7046415c333e225d37990b Mon Sep 17 00:00:00 2001 From: gennady Date: Wed, 15 Mar 2023 11:18:33 +0600 Subject: [PATCH 10/75] compare items in node --- .../scala/dev/rudiments/hardcore/CRUD.scala | 1 + .../scala/dev/rudiments/hardcore/Node.scala | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala b/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala index 6a490bc5..9a186490 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala @@ -35,6 +35,7 @@ case class Commit(events: (Location, Event with CRUD)*) extends Event with CRUD } case class NotFound(id: Location) extends Report with CRUD +case object Identical extends Report with CRUD case class Conflict(incoming: Event, actual: Out) extends Error with CRUD case class NotSupported(in: In) extends Error with CRUD case class MultiError(errors: (Location, Out)*) extends Error with CRUD diff --git a/core/src/main/scala/dev/rudiments/hardcore/Node.scala b/core/src/main/scala/dev/rudiments/hardcore/Node.scala index 2bdd22f3..56cd2c47 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Node.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Node.scala @@ -47,6 +47,36 @@ case class Node( } } + def compare(from: Location, to: Location): Out with CRUD = { + (read(from), read(to)) match + case (Readen(f: Node), Readen(t: Node)) => f.reconsileTo(t) match + case Nil => Identical + case evts => Commit(evts:_*) + case (_: NotFound, Readen(t)) => Created(t) + case (Readen(f), Readen(t)) if f != t => Updated(f, t) + case (_: Readen, _: Readen) => Identical + case (Readen(f), _: NotFound) => Deleted(f) + case (nf1: NotFound, _: NotFound) => nf1 + case other => throw new IllegalArgumentException(s"should never happen with $other") + } + + def reconsileTo(node: Node): Seq[(Location, Event with CRUD)] = { + if (this == node) { + Seq.empty + } else { + val keys = this.state.keySet ++ node.state.keySet + keys.foldLeft(Seq.empty[(Location, Event with CRUD)]) { (out, key) => + (this.state.get(key), node.state.get(key)) match + case (Some(f: Node), Some(t: Node)) => out ++ f.reconsileTo(t).map { (l, e) => key / l -> e } + case (None, Some(t)) => out :+ key -> Created(t) + case (Some(f), Some(t)) if f != t => out :+ key -> Updated(f, t) + case (Some(f), Some(t)) if f == t => out + case (Some(f), None) => out :+ key -> Deleted(f) + case other => throw new IllegalArgumentException(s"should never happen with $other") + } + } + } + def size: Int = this.state.size def >+ (pair: (Location, Product)): Out with CRUD = this.apply(pair._1, Created(pair._2)) From bd304b0eafe8a5effdb47da0c037bf29f05d944b Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 16 Mar 2023 20:49:38 +0600 Subject: [PATCH 11/75] sha-256 and sha-3 (256) wrappers --- .../scala/dev/rudiments/utils/Hashed.scala | 49 +++++++++++++++++++ .../test/dev/rudiments/utils/HashedTest.scala | 36 ++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 core/src/main/scala/dev/rudiments/utils/Hashed.scala create mode 100644 core/src/test/scala/test/dev/rudiments/utils/HashedTest.scala 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..f5bda7ed --- /dev/null +++ b/core/src/main/scala/dev/rudiments/utils/Hashed.scala @@ -0,0 +1,49 @@ +package dev.rudiments.utils + +import java.math.BigInteger +import java.nio.charset.Charset +import java.security.MessageDigest +import java.util.Base64 + +trait Hashed(hash: Array[Byte]) { + lazy val bigInteget: BigInteger = new BigInteger(1, hash) + lazy val string: String = String.format("%064x", bigInteget) + + override def toString: String = string + + override def hashCode(): Int = this.hash.toList.hashCode() +} + +object Hashed { + val utf8: Charset = Charset.forName("UTF-8") +} + +case class SHA256(hash: Array[Byte]) extends Hashed(hash) { + override def equals(obj: Any): Boolean = obj match { + case other: SHA256 => this.hash.sameElements(other.hash) + case _ => false + } +} + +object SHA256 { + val digester: MessageDigest = MessageDigest.getInstance("SHA-256") + + def apply(s: String): SHA256 = this.apply(s.getBytes(Hashed.utf8)) + def apply(b: Array[Byte]): SHA256 = new SHA256(digester.digest(b)) +} + + +case class SHA3(hash: Array[Byte]) extends Hashed(hash) { + override def equals(obj: Any): Boolean = obj match { + case other: SHA3 => this.hash.sameElements(other.hash) + case _ => false + } +} + +object SHA3 { + val digester: MessageDigest = MessageDigest.getInstance("SHA3-256") + + def apply(s: String): SHA3 = this.apply(s.getBytes(Hashed.utf8)) + + def apply(b: Array[Byte]): SHA3 = new SHA3(digester.digest(b)) +} 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..7a68cfbc --- /dev/null +++ b/core/src/test/scala/test/dev/rudiments/utils/HashedTest.scala @@ -0,0 +1,36 @@ +package test.dev.rudiments.utils + +import dev.rudiments.utils.{SHA256, SHA3} +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 HashedTest extends AnyWordSpec with Matchers { + "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) + } + } +} From e4048f50a1faa37d68928edd3cec24779bbd3447 Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 16 Mar 2023 20:49:49 +0600 Subject: [PATCH 12/75] add file module --- file/build.gradle | 3 +++ settings.gradle | 1 + 2 files changed, 4 insertions(+) create mode 100644 file/build.gradle diff --git a/file/build.gradle b/file/build.gradle new file mode 100644 index 00000000..e4dbb7fe --- /dev/null +++ b/file/build.gradle @@ -0,0 +1,3 @@ +dependencies { + implementation project(':core') +} diff --git a/settings.gradle b/settings.gradle index 272e48dc..cca81d3e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,5 @@ rootProject.name = 'hardcore' include 'core' include 'codec' +include 'file' include 'example' From 3b7d676e7531a7cfc27154ebb6bb027861e85aed Mon Sep 17 00:00:00 2001 From: gennady Date: Fri, 17 Mar 2023 12:24:28 +0600 Subject: [PATCH 13/75] add sha-1 for future git compatibility --- .../scala/dev/rudiments/hardcore/Tx.scala | 11 +++++++++ .../scala/dev/rudiments/utils/Hashed.scala | 17 +++++++++++++ .../test/dev/rudiments/utils/HashedTest.scala | 24 ++++++++++++++++++- 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 core/src/main/scala/dev/rudiments/hardcore/Tx.scala diff --git a/core/src/main/scala/dev/rudiments/hardcore/Tx.scala b/core/src/main/scala/dev/rudiments/hardcore/Tx.scala new file mode 100644 index 00000000..bb018bea --- /dev/null +++ b/core/src/main/scala/dev/rudiments/hardcore/Tx.scala @@ -0,0 +1,11 @@ +package dev.rudiments.hardcore + +import java.lang +import scala.collection.mutable + +case class Tx( + root: Node, + log: mutable.LinkedHashMap[Location, mutable.Seq[Out with CRUD]] +) { + +} diff --git a/core/src/main/scala/dev/rudiments/utils/Hashed.scala b/core/src/main/scala/dev/rudiments/utils/Hashed.scala index f5bda7ed..0d6756bf 100644 --- a/core/src/main/scala/dev/rudiments/utils/Hashed.scala +++ b/core/src/main/scala/dev/rudiments/utils/Hashed.scala @@ -18,6 +18,23 @@ object Hashed { val utf8: Charset = Charset.forName("UTF-8") } +case class SHA1(hash: Array[Byte]) extends Hashed(hash) { + override lazy val string: String = String.format("%032x", bigInteget) + + override def equals(obj: Any): Boolean = obj match { + case other: SHA1 => this.hash.sameElements(other.hash) + case _ => false + } +} + +object SHA1 { + val digester: MessageDigest = MessageDigest.getInstance("SHA-1") + + def apply(s: String): SHA1 = this.apply(s.getBytes(Hashed.utf8)) + + def apply(b: Array[Byte]): SHA1 = new SHA1(digester.digest(b)) +} + case class SHA256(hash: Array[Byte]) extends Hashed(hash) { override def equals(obj: Any): Boolean = obj match { case other: SHA256 => this.hash.sameElements(other.hash) diff --git a/core/src/test/scala/test/dev/rudiments/utils/HashedTest.scala b/core/src/test/scala/test/dev/rudiments/utils/HashedTest.scala index 7a68cfbc..0ef7f38d 100644 --- a/core/src/test/scala/test/dev/rudiments/utils/HashedTest.scala +++ b/core/src/test/scala/test/dev/rudiments/utils/HashedTest.scala @@ -1,6 +1,6 @@ package test.dev.rudiments.utils -import dev.rudiments.utils.{SHA256, SHA3} +import dev.rudiments.utils.{SHA256, SHA3, SHA1} import org.junit.runner.RunWith import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -8,6 +8,28 @@ import org.scalatestplus.junit.JUnitRunner @RunWith(classOf[JUnitRunner]) 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) + } + + "fit with git hash-object output" ignore { + val known = Map( + "git compatible" -> "be7296f5d069365d7b27f51d9fe31882efd584e9", + "sha-1" -> "abe18736ee5eb31f70fe61c8f08494553f71e62a" + ) + + val hashed = known.map((k, _) => k -> SHA1(k).toString) + hashed should be(known) + } + } + "SHA-256 hash" should { "fit with known hashes" in { val known = Map( From 02bd18e027cda0c6f8c78a7a8267a823ecf4546c Mon Sep 17 00:00:00 2001 From: gennady Date: Fri, 17 Mar 2023 17:17:55 +0600 Subject: [PATCH 14/75] git module, basic implementation of GitBlob with hashing --- .../test/dev/rudiments/utils/HashedTest.scala | 10 ------- git/build.gradle | 4 +++ .../scala/dev/rudiments/git/GitBlob.scala | 12 ++++++++ .../test/dev/rudiments/git/GitBlobTest.scala | 30 +++++++++++++++++++ settings.gradle | 1 + 5 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 git/build.gradle create mode 100644 git/src/main/scala/dev/rudiments/git/GitBlob.scala create mode 100644 git/src/test/scala/test/dev/rudiments/git/GitBlobTest.scala diff --git a/core/src/test/scala/test/dev/rudiments/utils/HashedTest.scala b/core/src/test/scala/test/dev/rudiments/utils/HashedTest.scala index 0ef7f38d..4e44afa0 100644 --- a/core/src/test/scala/test/dev/rudiments/utils/HashedTest.scala +++ b/core/src/test/scala/test/dev/rudiments/utils/HashedTest.scala @@ -18,16 +18,6 @@ class HashedTest extends AnyWordSpec with Matchers { val hashed = known.map((k, _) => k -> SHA1(k).toString) hashed should be(known) } - - "fit with git hash-object output" ignore { - val known = Map( - "git compatible" -> "be7296f5d069365d7b27f51d9fe31882efd584e9", - "sha-1" -> "abe18736ee5eb31f70fe61c8f08494553f71e62a" - ) - - val hashed = known.map((k, _) => k -> SHA1(k).toString) - hashed should be(known) - } } "SHA-256 hash" should { diff --git a/git/build.gradle b/git/build.gradle new file mode 100644 index 00000000..ce428f3b --- /dev/null +++ b/git/build.gradle @@ -0,0 +1,4 @@ +dependencies { + implementation project(':core') + implementation project(':file') +} diff --git a/git/src/main/scala/dev/rudiments/git/GitBlob.scala b/git/src/main/scala/dev/rudiments/git/GitBlob.scala new file mode 100644 index 00000000..81340788 --- /dev/null +++ b/git/src/main/scala/dev/rudiments/git/GitBlob.scala @@ -0,0 +1,12 @@ +package dev.rudiments.git + +import dev.rudiments.utils.{Hashed, SHA1} + +case class GitBlob(s: String, h: String, hash: SHA1) + +object GitBlob { + def apply(s: String): GitBlob = { + val h = "blob " + s.getBytes(Hashed.utf8).length + "\u0000" + new GitBlob(s, h, SHA1(h + s)) + } +} 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..90ff7559 --- /dev/null +++ b/git/src/test/scala/test/dev/rudiments/git/GitBlobTest.scala @@ -0,0 +1,30 @@ +package test.dev.rudiments.git + +import dev.rudiments.git.GitBlob +import dev.rudiments.utils.{SHA1, SHA256, SHA3} +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 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 = GitBlob("what is up, doc?") + blob.h should be ("blob 16\u0000") + 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 -> GitBlob(k).hash.toString) + hashed should be(known) + } + } +} diff --git a/settings.gradle b/settings.gradle index cca81d3e..8d83ea9a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,4 +2,5 @@ rootProject.name = 'hardcore' include 'core' include 'codec' include 'file' +include 'git' include 'example' From a3e9375e5ea70baf7d6a4ba343c59619aee1f2f0 Mon Sep 17 00:00:00 2001 From: gennady Date: Fri, 17 Mar 2023 21:23:15 +0600 Subject: [PATCH 15/75] improve git objects --- .../scala/dev/rudiments/git/GitBlob.scala | 22 ++++++++++++------- .../test/dev/rudiments/git/GitBlobTest.scala | 10 ++++----- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/git/src/main/scala/dev/rudiments/git/GitBlob.scala b/git/src/main/scala/dev/rudiments/git/GitBlob.scala index 81340788..c77f2ad4 100644 --- a/git/src/main/scala/dev/rudiments/git/GitBlob.scala +++ b/git/src/main/scala/dev/rudiments/git/GitBlob.scala @@ -2,11 +2,17 @@ package dev.rudiments.git import dev.rudiments.utils.{Hashed, SHA1} -case class GitBlob(s: String, h: String, hash: SHA1) - -object GitBlob { - def apply(s: String): GitBlob = { - val h = "blob " + s.getBytes(Hashed.utf8).length + "\u0000" - new GitBlob(s, h, SHA1(h + s)) - } -} +enum WriteStatus: + case Success + case Failure + +enum GitObject(content: String, kind: String): + val header: String = kind + " " + content.getBytes(Hashed.utf8).length + val fullContent: String = header + "\u0000" + content + val hash: SHA1 = SHA1(fullContent) + + case Blob(content: String) extends GitObject(content, "blob") + + //TODO fix content for tree and commit + case Tree(content: String) extends GitObject(content, "tree") + case Commit(content: String) extends GitObject(content, "commit") diff --git a/git/src/test/scala/test/dev/rudiments/git/GitBlobTest.scala b/git/src/test/scala/test/dev/rudiments/git/GitBlobTest.scala index 90ff7559..5d850b0d 100644 --- a/git/src/test/scala/test/dev/rudiments/git/GitBlobTest.scala +++ b/git/src/test/scala/test/dev/rudiments/git/GitBlobTest.scala @@ -1,7 +1,7 @@ package test.dev.rudiments.git -import dev.rudiments.git.GitBlob -import dev.rudiments.utils.{SHA1, SHA256, SHA3} +import dev.rudiments.git.GitObject +import dev.rudiments.utils.SHA1 import org.junit.runner.RunWith import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -12,8 +12,8 @@ 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 = GitBlob("what is up, doc?") - blob.h should be ("blob 16\u0000") + val blob = GitObject.Blob("what is up, doc?") + blob.header should be ("blob 16") blob.hash.toString should be("bd9dbf5aae1a3862dd1526723246b20206e5fc37") } @@ -23,7 +23,7 @@ class GitBlobTest extends AnyWordSpec with Matchers { "sha-1" -> "ea9090c10ac8e06b8d50114e6816042d5a7e16d8" ) - val hashed = known.map((k, _) => k -> GitBlob(k).hash.toString) + val hashed = known.map((k, _) => k -> GitObject.Blob(k).hash.toString) hashed should be(known) } } From 797ff6b20126bd409fe94528c9530512dc92af9a Mon Sep 17 00:00:00 2001 From: gennady Date: Sat, 18 Mar 2023 01:43:28 +0600 Subject: [PATCH 16/75] make git objects (blob, tree, commit) readable and writable --- .../scala/dev/rudiments/utils/Hashed.scala | 16 +- .../main/scala/dev/rudiments/utils/ZLib.scala | 42 ++++++ .../test/dev/rudiments/utils/HashedTest.scala | 6 + .../scala/dev/rudiments/git/GitBlob.scala | 18 --- .../scala/dev/rudiments/git/GitObject.scala | 142 ++++++++++++++++++ .../main/scala/dev/rudiments/git/Reader.scala | 40 +++++ .../main/scala/dev/rudiments/git/Writer.scala | 34 +++++ .../test/dev/rudiments/git/GitBlobTest.scala | 6 +- .../dev/rudiments/git/GitCommitsTest.scala | 31 ++++ .../dev/rudiments/git/GitObjectTest.scala | 56 +++++++ 10 files changed, 363 insertions(+), 28 deletions(-) create mode 100644 core/src/main/scala/dev/rudiments/utils/ZLib.scala delete mode 100644 git/src/main/scala/dev/rudiments/git/GitBlob.scala create mode 100644 git/src/main/scala/dev/rudiments/git/GitObject.scala create mode 100644 git/src/main/scala/dev/rudiments/git/Reader.scala create mode 100644 git/src/main/scala/dev/rudiments/git/Writer.scala create mode 100644 git/src/test/scala/test/dev/rudiments/git/GitCommitsTest.scala create mode 100644 git/src/test/scala/test/dev/rudiments/git/GitObjectTest.scala diff --git a/core/src/main/scala/dev/rudiments/utils/Hashed.scala b/core/src/main/scala/dev/rudiments/utils/Hashed.scala index 0d6756bf..9b074ac5 100644 --- a/core/src/main/scala/dev/rudiments/utils/Hashed.scala +++ b/core/src/main/scala/dev/rudiments/utils/Hashed.scala @@ -1,9 +1,9 @@ package dev.rudiments.utils import java.math.BigInteger -import java.nio.charset.Charset +import java.nio.charset.StandardCharsets.UTF_8 import java.security.MessageDigest -import java.util.Base64 +import java.util.{Base64, HexFormat} trait Hashed(hash: Array[Byte]) { lazy val bigInteget: BigInteger = new BigInteger(1, hash) @@ -15,11 +15,11 @@ trait Hashed(hash: Array[Byte]) { } object Hashed { - val utf8: Charset = Charset.forName("UTF-8") + val hexFormat: HexFormat = HexFormat.of() } case class SHA1(hash: Array[Byte]) extends Hashed(hash) { - override lazy val string: String = String.format("%032x", bigInteget) + override lazy val string: String = String.format("%040x", bigInteget) override def equals(obj: Any): Boolean = obj match { case other: SHA1 => this.hash.sameElements(other.hash) @@ -30,9 +30,11 @@ case class SHA1(hash: Array[Byte]) extends Hashed(hash) { object SHA1 { val digester: MessageDigest = MessageDigest.getInstance("SHA-1") - def apply(s: String): SHA1 = this.apply(s.getBytes(Hashed.utf8)) + def apply(s: String): SHA1 = this.apply(s.getBytes(UTF_8)) def apply(b: Array[Byte]): SHA1 = new SHA1(digester.digest(b)) + + def fromHex(hex: String): SHA1 = new SHA1(Hashed.hexFormat.parseHex(hex)) } case class SHA256(hash: Array[Byte]) extends Hashed(hash) { @@ -45,7 +47,7 @@ case class SHA256(hash: Array[Byte]) extends Hashed(hash) { object SHA256 { val digester: MessageDigest = MessageDigest.getInstance("SHA-256") - def apply(s: String): SHA256 = this.apply(s.getBytes(Hashed.utf8)) + def apply(s: String): SHA256 = this.apply(s.getBytes(UTF_8)) def apply(b: Array[Byte]): SHA256 = new SHA256(digester.digest(b)) } @@ -60,7 +62,7 @@ case class SHA3(hash: Array[Byte]) extends Hashed(hash) { object SHA3 { val digester: MessageDigest = MessageDigest.getInstance("SHA3-256") - def apply(s: String): SHA3 = this.apply(s.getBytes(Hashed.utf8)) + def apply(s: String): SHA3 = this.apply(s.getBytes(UTF_8)) def apply(b: Array[Byte]): SHA3 = new SHA3(digester.digest(b)) } 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..4aad4bd3 --- /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 BUFFER_SIZE = 4096 + + def pack(data: Array[Byte]): 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](BUFFER_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]): Array[Byte] = { + val inflater = new Inflater() + inflater.setInput(data) + + val outputStream = new ByteArrayOutputStream(data.length) + try { + val buffer = new Array[Byte](BUFFER_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/utils/HashedTest.scala b/core/src/test/scala/test/dev/rudiments/utils/HashedTest.scala index 4e44afa0..6bec0f7d 100644 --- a/core/src/test/scala/test/dev/rudiments/utils/HashedTest.scala +++ b/core/src/test/scala/test/dev/rudiments/utils/HashedTest.scala @@ -18,6 +18,12 @@ class HashedTest extends AnyWordSpec with Matchers { 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 { diff --git a/git/src/main/scala/dev/rudiments/git/GitBlob.scala b/git/src/main/scala/dev/rudiments/git/GitBlob.scala deleted file mode 100644 index c77f2ad4..00000000 --- a/git/src/main/scala/dev/rudiments/git/GitBlob.scala +++ /dev/null @@ -1,18 +0,0 @@ -package dev.rudiments.git - -import dev.rudiments.utils.{Hashed, SHA1} - -enum WriteStatus: - case Success - case Failure - -enum GitObject(content: String, kind: String): - val header: String = kind + " " + content.getBytes(Hashed.utf8).length - val fullContent: String = header + "\u0000" + content - val hash: SHA1 = SHA1(fullContent) - - case Blob(content: String) extends GitObject(content, "blob") - - //TODO fix content for tree and commit - case Tree(content: String) extends GitObject(content, "tree") - case Commit(content: String) extends GitObject(content, "commit") 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..fec27d36 --- /dev/null +++ b/git/src/main/scala/dev/rudiments/git/GitObject.scala @@ -0,0 +1,142 @@ +package dev.rudiments.git + +import dev.rudiments.utils.{Hashed, SHA1, ZLib} + +import java.lang +import java.lang.{IllegalStateException, StringBuffer} +import java.nio.charset.StandardCharsets.UTF_8 +import java.nio.file.{Files, Path} +import java.time.{Instant, LocalDateTime, ZoneId, ZonedDateTime} +import java.time.format.{DateTimeFormatter, DateTimeFormatterBuilder, SignStyle} +import java.time.temporal.ChronoField +import scala.collection.mutable + +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: String) extends GitObject("blob") { + override def data: Array[Byte] = content.getBytes(UTF_8) +} + +object Blob { + def apply(data: Array[Byte]): Blob = new Blob(new String(data, UTF_8)) +} + +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 :: Nil => items.addOne(Item(Mode(mode), name, new SHA1(data.slice(div + 1, div + 21)))) + 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 Executable extends Mode("100755") + case SymbolicLink extends Mode("120000") + case SubTree extends Mode("40000") + + 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: Option[SHA1], + author: Commit.AuthRecord, + committer: Commit.AuthRecord, + message: String +) extends GitObject("commit") { + override def data: Array[Byte] = { + s"""tree $tree + |parent ${parent.map(_.toString).getOrElse("NIL")} + |author $author + |committer $committer + | + |$message""".stripMargin + .getBytes(UTF_8) + } +} + +object Commit { + def apply(data: Array[Byte]): Commit = { + new String(data, UTF_8).split("\n\n").toList match { + case header :: message :: Nil => + header.split("\n").toList match { + case t :: p :: a :: c :: Nil + if t.startsWith("tree ") && + p.startsWith("parent ") && + a.startsWith("author ") && + c.startsWith("committer ") => + new Commit( + SHA1.fromHex(t.drop(5)), + if (p.drop(7) == "NIL") None else Some(SHA1.fromHex(p.drop(7))), + AuthRecord.parse(a.drop(7)), + AuthRecord.parse(c.drop(10)), + message + ) + case _ => throw new IllegalArgumentException("Can't read commit header") + } + case _ => throw new IllegalArgumentException("Can't read commit") + } + } + + 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 parse(s: String): AuthRecord = { + s.split(" ").toList match + case name :: mail :: time :: zone :: Nil => + val z = ZoneId.of(zone) + val when = Instant.ofEpochMilli(time.toLong).atZone(z) + new AuthRecord(name, mail, when) + } + } +} \ No newline at end of file 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..087e6de1 --- /dev/null +++ b/git/src/main/scala/dev/rudiments/git/Reader.scala @@ -0,0 +1,40 @@ +package dev.rudiments.git + +import dev.rudiments.utils.ZLib + +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 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/Writer.scala b/git/src/main/scala/dev/rudiments/git/Writer.scala new file mode 100644 index 00000000..927d4114 --- /dev/null +++ b/git/src/main/scala/dev/rudiments/git/Writer.scala @@ -0,0 +1,34 @@ +package dev.rudiments.git + +import dev.rudiments.utils.{Hashed, 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() + import java.nio.file.StandardOpenOption._ + 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 index 5d850b0d..952c943a 100644 --- a/git/src/test/scala/test/dev/rudiments/git/GitBlobTest.scala +++ b/git/src/test/scala/test/dev/rudiments/git/GitBlobTest.scala @@ -1,6 +1,6 @@ package test.dev.rudiments.git -import dev.rudiments.git.GitObject +import dev.rudiments.git.Blob import dev.rudiments.utils.SHA1 import org.junit.runner.RunWith import org.scalatest.matchers.should.Matchers @@ -12,7 +12,7 @@ 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 = GitObject.Blob("what is up, doc?") + val blob = Blob("what is up, doc?") blob.header should be ("blob 16") blob.hash.toString should be("bd9dbf5aae1a3862dd1526723246b20206e5fc37") } @@ -23,7 +23,7 @@ class GitBlobTest extends AnyWordSpec with Matchers { "sha-1" -> "ea9090c10ac8e06b8d50114e6816042d5a7e16d8" ) - val hashed = known.map((k, _) => k -> GitObject.Blob(k).hash.toString) + 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..790763be --- /dev/null +++ b/git/src/test/scala/test/dev/rudiments/git/GitCommitsTest.scala @@ -0,0 +1,31 @@ +package test.dev.rudiments.git + +import dev.rudiments.git.{Commit, Reader} +import dev.rudiments.utils.Log +import org.junit.runner.RunWith +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import org.scalatestplus.junit.JUnitRunner + +import java.nio.file.{Files, Path} + +@RunWith(classOf[JUnitRunner]) +class GitCommitsTest extends AnyWordSpec with Matchers with Log { + private val dir = Path.of("..").toAbsolutePath //TODO fix + + "can read chain of commits" 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.get.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/GitObjectTest.scala b/git/src/test/scala/test/dev/rudiments/git/GitObjectTest.scala new file mode 100644 index 00000000..b7ae08f0 --- /dev/null +++ b/git/src/test/scala/test/dev/rudiments/git/GitObjectTest.scala @@ -0,0 +1,56 @@ +package test.dev.rudiments.git + +import dev.rudiments.git.{Blob, Reader, Tree, Writer} +import dev.rudiments.utils.Log +import org.junit.runner.RunWith +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import org.scalatestplus.junit.JUnitRunner + +import java.nio.file.{Files, Path} + +@RunWith(classOf[JUnitRunner]) +class GitObjectTest extends AnyWordSpec with Matchers with Log { + private val dir = Path.of("..").toAbsolutePath //TODO fix + + "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") + } +} From dffa5328e594590286c0785016d4b5a36e493bf9 Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 23 Mar 2023 03:54:50 +0600 Subject: [PATCH 17/75] can read meta , trees and some commits --- .../main/scala/dev/rudiments/utils/CRC.scala | 7 ++ .../scala/dev/rudiments/git/GitObject.scala | 7 +- .../main/scala/dev/rudiments/git/Pack.scala | 109 ++++++++++++++++++ .../main/scala/dev/rudiments/git/Reader.scala | 2 + .../scala/dev/rudiments/git/Repository.scala | 47 ++++++++ .../dev/rudiments/git/GitCommitsTest.scala | 26 +++-- .../dev/rudiments/git/GitObjectTest.scala | 18 ++- .../test/dev/rudiments/git/PackTest.scala | 22 ++++ .../dev/rudiments/git/RepositoryTest.scala | 22 ++++ 9 files changed, 246 insertions(+), 14 deletions(-) create mode 100644 core/src/main/scala/dev/rudiments/utils/CRC.scala create mode 100644 git/src/main/scala/dev/rudiments/git/Pack.scala create mode 100644 git/src/main/scala/dev/rudiments/git/Repository.scala create mode 100644 git/src/test/scala/test/dev/rudiments/git/PackTest.scala create mode 100644 git/src/test/scala/test/dev/rudiments/git/RepositoryTest.scala 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/git/src/main/scala/dev/rudiments/git/GitObject.scala b/git/src/main/scala/dev/rudiments/git/GitObject.scala index fec27d36..b5c4b587 100644 --- a/git/src/main/scala/dev/rudiments/git/GitObject.scala +++ b/git/src/main/scala/dev/rudiments/git/GitObject.scala @@ -115,7 +115,8 @@ object Commit { ) case _ => throw new IllegalArgumentException("Can't read commit header") } - case _ => throw new IllegalArgumentException("Can't read commit") + case other => //TODO fails of merge with pgp + throw new IllegalArgumentException("Can't read commit") } } @@ -137,6 +138,10 @@ object Commit { val z = ZoneId.of(zone) val when = Instant.ofEpochMilli(time.toLong).atZone(z) new AuthRecord(name, mail, when) + case name1 :: name2 :: mail :: time :: zone :: Nil => + val z = ZoneId.of(zone) + val when = Instant.ofEpochMilli(time.toLong).atZone(z) + new AuthRecord(name1 + " " + name2, mail, when) } } } \ No newline at end of file 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..51d6aa6d --- /dev/null +++ b/git/src/main/scala/dev/rudiments/git/Pack.scala @@ -0,0 +1,109 @@ +package dev.rudiments.git + +import dev.rudiments.utils.{CRC, SHA1, ZLib} + +import java.io.{FileInputStream, InputStream} +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets.UTF_8 +import java.nio.file.{Files, Path} +import java.nio.file.StandardOpenOption.READ +import scala.util.{Failure, Success} + +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(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(Array.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 = { + 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 index 087e6de1..cb0b4867 100644 --- a/git/src/main/scala/dev/rudiments/git/Reader.scala +++ b/git/src/main/scala/dev/rudiments/git/Reader.scala @@ -2,6 +2,7 @@ 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} @@ -30,6 +31,7 @@ object Reader { 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")) } 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..d4f66545 --- /dev/null +++ b/git/src/main/scala/dev/rudiments/git/Repository.scala @@ -0,0 +1,47 @@ +package dev.rudiments.git + +import java.nio.charset.StandardCharsets.UTF_8 +import dev.rudiments.git.Pack.{Entry, PackObj} +import dev.rudiments.utils.{Log, SHA1, ZLib} + +import java.nio.file.{Files, Path} +import java.util.stream.Collectors +import scala.jdk.CollectionConverters._ +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 + + 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 = Pack.readPack(root, hash).objects.foreach { + case (key, Entry(PackObj.Tree, _, data, _, _)) => + objects.put(key, Tree(ZLib.unpack(data))) + case (key, Entry(PackObj.Commit, _, data, _, _)) => + objects.put(key, Commit(ZLib.unpack(data))) + case (key, Entry(PackObj.Blob, _, data, _, _)) => + objects.put(key, Blob(ZLib.unpack(data))) + + case (key, Entry(PackObj.Tag, _, _, _, _)) => key -> ??? + case (key, entry) => // filtered + } +} diff --git a/git/src/test/scala/test/dev/rudiments/git/GitCommitsTest.scala b/git/src/test/scala/test/dev/rudiments/git/GitCommitsTest.scala index 790763be..abdf3419 100644 --- a/git/src/test/scala/test/dev/rudiments/git/GitCommitsTest.scala +++ b/git/src/test/scala/test/dev/rudiments/git/GitCommitsTest.scala @@ -13,19 +13,21 @@ 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" 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.get.toString) - } yield (first, tree, second) + "can read chain of commits till the first one" ignore { + var h = "a3e9375e5ea70baf7d6a4ba343c59619aee1f2f0" + var i = 0; // up to 37 - 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") + while(h != "SUCCESS" || h != "FAIL") { + Reader.read(dir, h) match { + case Right(c: Commit) if c.parent.isDefined => + h = c.parent.get.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 index b7ae08f0..66759085 100644 --- a/git/src/test/scala/test/dev/rudiments/git/GitObjectTest.scala +++ b/git/src/test/scala/test/dev/rudiments/git/GitObjectTest.scala @@ -1,6 +1,6 @@ package test.dev.rudiments.git -import dev.rudiments.git.{Blob, Reader, Tree, Writer} +import dev.rudiments.git.{Blob, Commit, Reader, Tree, Writer} import dev.rudiments.utils.Log import org.junit.runner.RunWith import org.scalatest.matchers.should.Matchers @@ -53,4 +53,20 @@ class GitObjectTest extends AnyWordSpec with Matchers with Log { 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.get.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..c2007242 --- /dev/null +++ b/git/src/test/scala/test/dev/rudiments/git/PackTest.scala @@ -0,0 +1,22 @@ +package test.dev.rudiments.git + +import dev.rudiments.git.Pack +import dev.rudiments.git.Pack.PackObj +import dev.rudiments.utils.Log +import org.junit.runner.RunWith +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import org.scalatestplus.junit.JUnitRunner + +import java.nio.file.{Files, Path} + +@RunWith(classOf[JUnitRunner]) +class PackTest extends AnyWordSpec with Matchers with Log { + private val dir = Path.of("..").toAbsolutePath //TODO fix + + "can read pack index" in { + val hash = "8cc0a2aa174783656e7d32edb2993b578c957c2d" + val readen = Pack.readPack(dir, hash) + readen.objects.size should be (367) + } +} 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..4aeeb804 --- /dev/null +++ b/git/src/test/scala/test/dev/rudiments/git/RepositoryTest.scala @@ -0,0 +1,22 @@ +package test.dev.rudiments.git + +import dev.rudiments.git.{Pack, Repository} +import dev.rudiments.git.Pack.PackObj +import dev.rudiments.utils.Log +import org.junit.runner.RunWith +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import org.scalatestplus.junit.JUnitRunner + +import java.nio.file.{Files, Path} + +@RunWith(classOf[JUnitRunner]) +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" ignore { + repo.read() //TODO fails of merge commit with pgp + } +} From 2ffaca8d24cd2c2f77507d489a5c83c1b1eab45d Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 30 Mar 2023 03:14:31 +0600 Subject: [PATCH 18/75] WIP: can't read deltas and initial commit --- .../scala/dev/rudiments/git/GitObject.scala | 120 +++++++++++++++--- .../scala/dev/rudiments/git/Repository.scala | 9 +- .../dev/rudiments/git/GitObjectTest.scala | 96 +++++++------- .../dev/rudiments/git/RepositoryTest.scala | 4 +- 4 files changed, 161 insertions(+), 68 deletions(-) diff --git a/git/src/main/scala/dev/rudiments/git/GitObject.scala b/git/src/main/scala/dev/rudiments/git/GitObject.scala index b5c4b587..f34d7c03 100644 --- a/git/src/main/scala/dev/rudiments/git/GitObject.scala +++ b/git/src/main/scala/dev/rudiments/git/GitObject.scala @@ -10,6 +10,7 @@ import java.time.{Instant, LocalDateTime, ZoneId, ZonedDateTime} import java.time.format.{DateTimeFormatter, DateTimeFormatterBuilder, SignStyle} import java.time.temporal.ChronoField import scala.collection.mutable +import scala.util.matching.Regex sealed trait GitObject(kind: String) { val header: String = s"$kind $size" @@ -66,10 +67,13 @@ object Tree { 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")) + values.find(_.code == code).getOrElse { + throw new IllegalArgumentException(s"Not a mode code: $code") + } } case class Item(mode: Mode, name: String, hash: SHA1) { @@ -97,26 +101,108 @@ final case class Commit( } object Commit { + val signedPattern: Regex = + """tree (\w{40}) + |parent (\w{40}) + |author (.+) <(.+)> (\d{10,20}) (.+) + |committer (.+) <(.+)> (\d{10,20}) (.+) + |gpgsig -----BEGIN PGP SIGNATURE----- + |\s* + | .{64} + | .{64} + | .{64} + | .{64} + | .{64} + | .{64} + | .{1,64} + | -----END PGP SIGNATURE----- + |\s* + |\s* + |(.*)""".stripMargin.r + + val signed2Pattern: Regex = + """tree (\w{40}) + |parent (\w{40}) + |(parent (\w{40}))? + |author (.+) <(.+)> (\d{10,20}) (.+) + |committer (.+) <(.+)> (\d{10,20}) (.+) + |gpgsig -----BEGIN PGP SIGNATURE----- + |\s* + | .{64} + | .{64} + | .{64} + | .{64} + | .{64} + | .{64} + | .{1,64} + | -----END PGP SIGNATURE----- + |\s* + |\s* + |(.*)""".stripMargin.r + + val commitPattern: Regex = + """tree (\w{40}) + |parent (\w{40}) + |author (.+) <(.+)> (\d{10,20}) (.+) + |committer (.+) <(.+)> (\d{10,20}) (.+) + | + |(.*)""".stripMargin.r + + val mergePattern: Regex = + """tree (\w{40}) + |parent (\w{40}) + |(parent (\w{40}))? + |author (.+) <(.+)> (\d{10,20}) (.+) + |committer (.+) <(.+)> (\d{10,20}) (.+) + | + |(.*)""".stripMargin.r + def apply(data: Array[Byte]): Commit = { - new String(data, UTF_8).split("\n\n").toList match { - case header :: message :: Nil => - header.split("\n").toList match { - case t :: p :: a :: c :: Nil - if t.startsWith("tree ") && - p.startsWith("parent ") && - a.startsWith("author ") && - c.startsWith("committer ") => + val asString = new String(data, UTF_8) + try { + commitPattern.findFirstMatchIn(asString).map { m => + new Commit( + SHA1.fromHex(m.group(1)), + Some(SHA1.fromHex(m.group(2))), + AuthRecord(m.group(3), m.group(4), Instant.ofEpochMilli(m.group(5).toLong).atZone(ZoneId.of(m.group(6)))), + AuthRecord(m.group(7), m.group(8), Instant.ofEpochMilli(m.group(9).toLong).atZone(ZoneId.of(m.group(10)))), + m.group(11) + ) + }.getOrElse { + mergePattern.findFirstMatchIn(asString).map { m => + new Commit( + SHA1.fromHex(m.group(1)), + Some(SHA1.fromHex(m.group(2))), + AuthRecord(m.group(5), m.group(6), Instant.ofEpochMilli(m.group(7).toLong).atZone(ZoneId.of(m.group(8)))), + AuthRecord(m.group(9), m.group(10), Instant.ofEpochMilli(m.group(11).toLong).atZone(ZoneId.of(m.group(12)))), + m.group(13) + ) + }.getOrElse { + signedPattern.findFirstMatchIn(asString).map { m => new Commit( - SHA1.fromHex(t.drop(5)), - if (p.drop(7) == "NIL") None else Some(SHA1.fromHex(p.drop(7))), - AuthRecord.parse(a.drop(7)), - AuthRecord.parse(c.drop(10)), - message + SHA1.fromHex(m.group(1)), + Some(SHA1.fromHex(m.group(2))), + AuthRecord(m.group(3), m.group(4), Instant.ofEpochMilli(m.group(5).toLong).atZone(ZoneId.of(m.group(6)))), + AuthRecord(m.group(7), m.group(8), Instant.ofEpochMilli(m.group(9).toLong).atZone(ZoneId.of(m.group(10)))), + m.group(11) ) - case _ => throw new IllegalArgumentException("Can't read commit header") + }.getOrElse { + signed2Pattern.findFirstMatchIn(asString).map { m => + new Commit( + SHA1.fromHex(m.group(1)), + Some(SHA1.fromHex(m.group(2))), + AuthRecord(m.group(5), m.group(6), Instant.ofEpochMilli(m.group(7).toLong).atZone(ZoneId.of(m.group(8)))), + AuthRecord(m.group(9), m.group(10), Instant.ofEpochMilli(m.group(11).toLong).atZone(ZoneId.of(m.group(12)))), + m.group(13) + ) + }.getOrElse { + throw new IllegalArgumentException(s"Can't read commit: $asString") + } + } } - case other => //TODO fails of merge with pgp - throw new IllegalArgumentException("Can't read commit") + } + } catch { + case e: Exception => throw e } } diff --git a/git/src/main/scala/dev/rudiments/git/Repository.scala b/git/src/main/scala/dev/rudiments/git/Repository.scala index d4f66545..1a0c87ad 100644 --- a/git/src/main/scala/dev/rudiments/git/Repository.scala +++ b/git/src/main/scala/dev/rudiments/git/Repository.scala @@ -37,11 +37,16 @@ class Repository(root: Path) extends Log { case (key, Entry(PackObj.Tree, _, data, _, _)) => objects.put(key, Tree(ZLib.unpack(data))) case (key, Entry(PackObj.Commit, _, data, _, _)) => - objects.put(key, Commit(ZLib.unpack(data))) + try { + objects.put(key, Commit(ZLib.unpack(data))) + } catch { + case e: Exception => log.error("Failed commit {}", key) + } + case (key, Entry(PackObj.Blob, _, data, _, _)) => objects.put(key, Blob(ZLib.unpack(data))) - case (key, Entry(PackObj.Tag, _, _, _, _)) => key -> ??? + case (key, Entry(PackObj.Tag, _, _, _, _)) => // TODO case (key, entry) => // filtered } } diff --git a/git/src/test/scala/test/dev/rudiments/git/GitObjectTest.scala b/git/src/test/scala/test/dev/rudiments/git/GitObjectTest.scala index 66759085..8699d55c 100644 --- a/git/src/test/scala/test/dev/rudiments/git/GitObjectTest.scala +++ b/git/src/test/scala/test/dev/rudiments/git/GitObjectTest.scala @@ -13,60 +13,62 @@ import java.nio.file.{Files, Path} class GitObjectTest extends AnyWordSpec with Matchers with Log { private val dir = Path.of("..").toAbsolutePath //TODO fix - "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")) - } + "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 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 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) + "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)) - } + 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 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.get.toString) - } yield (first, tree, second) + "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.get.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") + 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/RepositoryTest.scala b/git/src/test/scala/test/dev/rudiments/git/RepositoryTest.scala index 4aeeb804..a46c20c4 100644 --- a/git/src/test/scala/test/dev/rudiments/git/RepositoryTest.scala +++ b/git/src/test/scala/test/dev/rudiments/git/RepositoryTest.scala @@ -16,7 +16,7 @@ class RepositoryTest extends AnyWordSpec with Matchers with Log { private val repo = new Repository(dir) - "can read packs" ignore { - repo.read() //TODO fails of merge commit with pgp + "can read packs" in { + repo.read() //TODO fails on initial commit } } From a35874e5139d5e212d57569b8a7c1157bc7f504e Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 20 Apr 2023 00:36:41 +0600 Subject: [PATCH 19/75] update gradle to 8.1 --- gradle/wrapper/gradle-wrapper.jar | Bin 59821 -> 62076 bytes gradle/wrapper/gradle-wrapper.properties | 3 ++- gradlew | 25 ++++++++++++++++------- gradlew.bat | 15 ++++++++------ 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 41d9927a4d4fb3f96a785543079b8df6723c946b..c1962a79e29d3e0ab67b14947c167a862655af9b 100644 GIT binary patch delta 40199 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 delta 37942 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/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 From 75e15d0de8066328f11a5d286f8a6c751be7d2f2 Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 20 Apr 2023 00:38:17 +0600 Subject: [PATCH 20/75] draft read files --- .../scala/dev/rudiments/utils/Hashed.scala | 19 +++--- .../main/scala/dev/rudiments/file/File.scala | 62 +++++++++++++++++++ .../scala/dev/rudiments/file/Header.scala | 48 ++++++++++++++ file/src/test/resources/example/1.txt | 1 + file/src/test/resources/example/nested/2.txt | 1 + .../test/dev/rudiments/file/FileTest.scala | 42 +++++++++++++ target.md | 58 +++++++++++++++++ 7 files changed, 223 insertions(+), 8 deletions(-) create mode 100644 file/src/main/scala/dev/rudiments/file/File.scala create mode 100644 file/src/main/scala/dev/rudiments/file/Header.scala create mode 100644 file/src/test/resources/example/1.txt create mode 100644 file/src/test/resources/example/nested/2.txt create mode 100644 file/src/test/scala/test/dev/rudiments/file/FileTest.scala create mode 100644 target.md diff --git a/core/src/main/scala/dev/rudiments/utils/Hashed.scala b/core/src/main/scala/dev/rudiments/utils/Hashed.scala index 9b074ac5..2cd66750 100644 --- a/core/src/main/scala/dev/rudiments/utils/Hashed.scala +++ b/core/src/main/scala/dev/rudiments/utils/Hashed.scala @@ -5,9 +5,9 @@ import java.nio.charset.StandardCharsets.UTF_8 import java.security.MessageDigest import java.util.{Base64, HexFormat} -trait Hashed(hash: Array[Byte]) { - lazy val bigInteget: BigInteger = new BigInteger(1, hash) - lazy val string: String = String.format("%064x", bigInteget) +sealed trait Hashed(hash: Array[Byte]) { + lazy val bigInteger: BigInteger = new BigInteger(1, hash) + lazy val string: String = String.format("%064x", bigInteger) override def toString: String = string @@ -18,8 +18,8 @@ object Hashed { val hexFormat: HexFormat = HexFormat.of() } -case class SHA1(hash: Array[Byte]) extends Hashed(hash) { - override lazy val string: String = String.format("%040x", bigInteget) +final case class SHA1(hash: Array[Byte]) extends Hashed(hash) { + override lazy val string: String = String.format("%040x", bigInteger) override def equals(obj: Any): Boolean = obj match { case other: SHA1 => this.hash.sameElements(other.hash) @@ -37,7 +37,7 @@ object SHA1 { def fromHex(hex: String): SHA1 = new SHA1(Hashed.hexFormat.parseHex(hex)) } -case class SHA256(hash: Array[Byte]) extends Hashed(hash) { +final case class SHA256(hash: Array[Byte]) extends Hashed(hash) { override def equals(obj: Any): Boolean = obj match { case other: SHA256 => this.hash.sameElements(other.hash) case _ => false @@ -49,10 +49,12 @@ object SHA256 { def apply(s: String): SHA256 = this.apply(s.getBytes(UTF_8)) def apply(b: Array[Byte]): SHA256 = new SHA256(digester.digest(b)) + + def fromHex(hex: String): SHA256 = new SHA256(Hashed.hexFormat.parseHex(hex)) } -case class SHA3(hash: Array[Byte]) extends Hashed(hash) { +final case class SHA3(hash: Array[Byte]) extends Hashed(hash) { override def equals(obj: Any): Boolean = obj match { case other: SHA3 => this.hash.sameElements(other.hash) case _ => false @@ -63,6 +65,7 @@ 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(digester.digest(b)) + + def fromHex(hex: String): SHA3 = new SHA3(Hashed.hexFormat.parseHex(hex)) } diff --git a/file/src/main/scala/dev/rudiments/file/File.scala b/file/src/main/scala/dev/rudiments/file/File.scala new file mode 100644 index 00000000..8db091aa --- /dev/null +++ b/file/src/main/scala/dev/rudiments/file/File.scala @@ -0,0 +1,62 @@ +package dev.rudiments.file + +import dev.rudiments.utils.SHA3 + +import java.io.File +import java.nio.ByteBuffer +import java.nio.file.{Files, Path} +import java.nio.charset.StandardCharsets.UTF_8 + + +sealed trait Files(val header: Header) {} +object Files { + def apply(path: Path): Files = { + val file = path.toFile + if(file.isDirectory) { + Many.apply(file.getName, file.listFiles().toList) + } else { + One.apply(file.getName, java.nio.file.Files.readAllBytes(file.toPath)) + } + } +} + +case class One(override val header: Header, content: Barr) extends Files(header) { + override def equals(obj: Any): Boolean = obj match + case One(h, c) => this.header.equals(h) && this.content.sameElements(c) + case _ => false +} +object One { + def apply(path: Path): One = { + val file = path.toFile + if(file.isFile) { + One.apply(file.getName, java.nio.file.Files.readAllBytes(file.toPath)) + } else { + throw new IllegalArgumentException("Not a file") + } + } + + def apply(name: String, content: Barr): One = new One(Header(name, Header.Type.File, content.length, SHA3(content)), content) +} + +case class Many(self: Header, items: Seq[Header]) extends Files(self) {} +object Many { + def apply(path: Path): Many = { + val file = path.toFile + if (file.isDirectory) { + Many.apply(file.getName, file.listFiles().toList) + } else { + throw new IllegalArgumentException("Not a directory") + } + } + + def apply(name: String, files: List[File]): Many = { + val readen = files.map(f => Files(f.toPath).header) + val size = readen.foldLeft(0){ (acc, i) => acc + i.size } + val bytes = readen.foldLeft(Array.empty[Byte]){ (acc, i) => acc ++ i.toByteArray} + new Many( + new Header(name, Header.Type.Dir, size, SHA3(bytes)), + readen + ) + } +} + diff --git a/file/src/main/scala/dev/rudiments/file/Header.scala b/file/src/main/scala/dev/rudiments/file/Header.scala new file mode 100644 index 00000000..c4b8d827 --- /dev/null +++ b/file/src/main/scala/dev/rudiments/file/Header.scala @@ -0,0 +1,48 @@ +package dev.rudiments.file + +import dev.rudiments.utils.SHA3 + +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets.UTF_8 + + +type Barr = Array[Byte] + +case class Header( + name: String, + fileType: Header.Type, + size: Int, + checksum: SHA3 +) { + def toByteArray: Barr = { + val nameBytes = name.getBytes(UTF_8) + ByteBuffer.allocate(nameBytes.length + 1 + 32 + 4) + .put(checksum.hash) + .put((fileType.ordinal + 1).toByte) + .putInt(size) + .put(nameBytes) + .array() + } +} + +object Header { + def apply(data: Barr): Header = { + val buff = ByteBuffer.wrap(data) + val hash = new Barr(32) + buff.get(hash) + val fileType = Header.Type(buff.get()) + val size = buff.getInt + val name = buff.asCharBuffer().toString + new Header(name, fileType, size, new SHA3(hash)) + } + + enum Type: + case File, Dir; + + object Type { + def apply(b: Byte): Type = b match + case 1 => File + case 2 => Dir + case _ => throw new IllegalArgumentException(s"Not supported type from header: $b") + } +} 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/scala/test/dev/rudiments/file/FileTest.scala b/file/src/test/scala/test/dev/rudiments/file/FileTest.scala new file mode 100644 index 00000000..8045d740 --- /dev/null +++ b/file/src/test/scala/test/dev/rudiments/file/FileTest.scala @@ -0,0 +1,42 @@ +package test.dev.rudiments.file + +import dev.rudiments.file.* +import dev.rudiments.utils.SHA3 +import org.junit.runner.RunWith +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import org.scalatestplus.junit.JUnitRunner + +import java.nio.charset.StandardCharsets.UTF_8 +import java.nio.file.Path + + +@RunWith(classOf[JUnitRunner]) +class FileTest extends AnyWordSpec with Matchers { + private val dir = Path.of("..", "file", "src", "test", "resources").toAbsolutePath + + private val f1 = new Header("1.txt", Header.Type.File, 10, SHA3("first file".getBytes(UTF_8))) + private val f2 = new Header("2.txt", Header.Type.File,11, SHA3("second file".getBytes(UTF_8))) + private val nested = new Header("nested", Header.Type.Dir, 11, SHA3(f2.toByteArray)) + + "can read single file" in { + Files.apply(dir.resolve(Path.of("example", "1.txt"))) should be( + new One(f1, "first file".getBytes(UTF_8)) + ) + } + + "can read directory with file" in { + Files.apply(dir.resolve(Path.of("example", "nested"))) should be ( + new Many(nested, Seq(f2)) + ) + } + + "can read nested directory" in { + Files.apply(dir.resolve("example")) should be( + new Many( + new Header("example", Header.Type.Dir, 21, SHA3(f1.toByteArray ++ nested.toByteArray)), + Seq(f1, nested) + ) + ) + } +} 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 From df74cb94ecb4ae2cca5c53076699147dd9b6b3ab Mon Sep 17 00:00:00 2001 From: gennady Date: Fri, 21 Apr 2023 22:07:23 +0600 Subject: [PATCH 21/75] use Seq instead of Array in hashes --- .../scala/dev/rudiments/utils/Hashed.scala | 40 ++++++------------- .../scala/dev/rudiments/file/Header.scala | 5 ++- .../main/scala/dev/rudiments/git/Pack.scala | 5 ++- 3 files changed, 19 insertions(+), 31 deletions(-) diff --git a/core/src/main/scala/dev/rudiments/utils/Hashed.scala b/core/src/main/scala/dev/rudiments/utils/Hashed.scala index 2cd66750..f609a0b4 100644 --- a/core/src/main/scala/dev/rudiments/utils/Hashed.scala +++ b/core/src/main/scala/dev/rudiments/utils/Hashed.scala @@ -4,9 +4,11 @@ import java.math.BigInteger import java.nio.charset.StandardCharsets.UTF_8 import java.security.MessageDigest import java.util.{Base64, HexFormat} +import scala.collection.immutable.ArraySeq -sealed trait Hashed(hash: Array[Byte]) { - lazy val bigInteger: BigInteger = new BigInteger(1, hash) +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 @@ -18,54 +20,38 @@ object Hashed { val hexFormat: HexFormat = HexFormat.of() } -final case class SHA1(hash: Array[Byte]) extends Hashed(hash) { +final case class SHA1(hash: Seq[Byte]) extends Hashed(hash) { override lazy val string: String = String.format("%040x", bigInteger) - - override def equals(obj: Any): Boolean = obj match { - case other: SHA1 => this.hash.sameElements(other.hash) - case _ => false - } } 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 apply(b: Array[Byte]): SHA1 = new SHA1(digester.digest(b)) - - def fromHex(hex: String): SHA1 = new SHA1(Hashed.hexFormat.parseHex(hex)) + def fromHex(hex: String): SHA1 = new SHA1(ArraySeq.unsafeWrapArray(Hashed.hexFormat.parseHex(hex))) } -final case class SHA256(hash: Array[Byte]) extends Hashed(hash) { - override def equals(obj: Any): Boolean = obj match { - case other: SHA256 => this.hash.sameElements(other.hash) - case _ => false - } -} +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(digester.digest(b)) + def apply(b: Array[Byte]): SHA256 = new SHA256(ArraySeq.unsafeWrapArray(digester.digest(b))) - def fromHex(hex: String): SHA256 = new SHA256(Hashed.hexFormat.parseHex(hex)) + def fromHex(hex: String): SHA256 = new SHA256(ArraySeq.unsafeWrapArray(Hashed.hexFormat.parseHex(hex))) } -final case class SHA3(hash: Array[Byte]) extends Hashed(hash) { - override def equals(obj: Any): Boolean = obj match { - case other: SHA3 => this.hash.sameElements(other.hash) - case _ => false - } -} +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(digester.digest(b)) + def apply(b: Array[Byte]): SHA3 = new SHA3(ArraySeq.unsafeWrapArray(digester.digest(b))) - def fromHex(hex: String): SHA3 = new SHA3(Hashed.hexFormat.parseHex(hex)) + def fromHex(hex: String): SHA3 = new SHA3(ArraySeq.unsafeWrapArray(Hashed.hexFormat.parseHex(hex))) } diff --git a/file/src/main/scala/dev/rudiments/file/Header.scala b/file/src/main/scala/dev/rudiments/file/Header.scala index c4b8d827..e8f05534 100644 --- a/file/src/main/scala/dev/rudiments/file/Header.scala +++ b/file/src/main/scala/dev/rudiments/file/Header.scala @@ -4,6 +4,7 @@ import dev.rudiments.utils.SHA3 import java.nio.ByteBuffer import java.nio.charset.StandardCharsets.UTF_8 +import scala.collection.immutable.ArraySeq type Barr = Array[Byte] @@ -17,7 +18,7 @@ case class Header( def toByteArray: Barr = { val nameBytes = name.getBytes(UTF_8) ByteBuffer.allocate(nameBytes.length + 1 + 32 + 4) - .put(checksum.hash) + .put(checksum.asArray) .put((fileType.ordinal + 1).toByte) .putInt(size) .put(nameBytes) @@ -33,7 +34,7 @@ object Header { val fileType = Header.Type(buff.get()) val size = buff.getInt val name = buff.asCharBuffer().toString - new Header(name, fileType, size, new SHA3(hash)) + new Header(name, fileType, size, new SHA3(ArraySeq.unsafeWrapArray(hash))) } enum Type: diff --git a/git/src/main/scala/dev/rudiments/git/Pack.scala b/git/src/main/scala/dev/rudiments/git/Pack.scala index 51d6aa6d..e6720345 100644 --- a/git/src/main/scala/dev/rudiments/git/Pack.scala +++ b/git/src/main/scala/dev/rudiments/git/Pack.scala @@ -7,6 +7,7 @@ import java.nio.ByteBuffer import java.nio.charset.StandardCharsets.UTF_8 import java.nio.file.{Files, Path} import java.nio.file.StandardOpenOption.READ +import scala.collection.immutable.ArraySeq import scala.util.{Failure, Success} case class Pack(objects: List[(SHA1, Pack.Entry)]) { @@ -39,7 +40,7 @@ object Pack { 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(h)).toList + 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 @@ -58,7 +59,7 @@ object Pack { val idx = readIdx(repo, hash, count) if(idx.nonEmpty) { - val tail = new SHA1(Array.empty) -> (bytes.length - 20) + 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) From 41bd5f86e5df10b028f83af4852e5d79ccc284e9 Mon Sep 17 00:00:00 2001 From: gennady Date: Mon, 24 Apr 2023 01:43:01 +0600 Subject: [PATCH 22/75] rework for repo --- codec/build.gradle | 3 +- .../dev/rudiments/utils/BytesCodec.scala | 26 ++++++++ file/build.gradle | 1 + .../file/{Header.scala => About.scala} | 24 +++---- .../main/scala/dev/rudiments/file/File.scala | 62 ------------------ .../scala/dev/rudiments/file/Repository.scala | 65 +++++++++++++++++++ .../test/dev/rudiments/file/FileTest.scala | 37 ++++------- git/build.gradle | 1 + 8 files changed, 117 insertions(+), 102 deletions(-) create mode 100644 core/src/main/scala/dev/rudiments/utils/BytesCodec.scala rename file/src/main/scala/dev/rudiments/file/{Header.scala => About.scala} (57%) delete mode 100644 file/src/main/scala/dev/rudiments/file/File.scala create mode 100644 file/src/main/scala/dev/rudiments/file/Repository.scala diff --git a/codec/build.gradle b/codec/build.gradle index 880dc354..ea8c1f25 100644 --- a/codec/build.gradle +++ b/codec/build.gradle @@ -1,5 +1,6 @@ dependencies { + implementation project(':core') + implementation 'io.circe:circe-core_3:0.14.5' implementation 'io.circe:circe-generic_3:0.14.5' - implementation 'io.circe:circe-generic-extras_3:0.14.5' } \ No newline at end of file diff --git a/core/src/main/scala/dev/rudiments/utils/BytesCodec.scala b/core/src/main/scala/dev/rudiments/utils/BytesCodec.scala new file mode 100644 index 00000000..03af1899 --- /dev/null +++ b/core/src/main/scala/dev/rudiments/utils/BytesCodec.scala @@ -0,0 +1,26 @@ +package dev.rudiments.utils + +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets.UTF_8 + +object BytesCodec { + def encodeStrings(strings: Seq[String]): Array[Byte] = { + val bytes = strings.map { s => + s.getBytes(UTF_8) + } + val size = 4 + bytes.size * 4 + bytes.map(_.length).sum // arr size + each element size + data + val buff = ByteBuffer.allocate(size).putInt(bytes.size) + bytes.foreach(b => buff.putInt(b.length).put(b)) + buff.array() + } + + def decodeString(bytes: Array[Byte]): Seq[String] = { + val buff = ByteBuffer.wrap(bytes) + (0 to buff.getInt).map { _ => + val size = buff.getInt + val arr = new Array[Byte](size) + buff.get(arr) + new String(arr, UTF_8) + } + } +} diff --git a/file/build.gradle b/file/build.gradle index e4dbb7fe..f2dd804a 100644 --- a/file/build.gradle +++ b/file/build.gradle @@ -1,3 +1,4 @@ dependencies { implementation project(':core') + implementation project(':codec') } diff --git a/file/src/main/scala/dev/rudiments/file/Header.scala b/file/src/main/scala/dev/rudiments/file/About.scala similarity index 57% rename from file/src/main/scala/dev/rudiments/file/Header.scala rename to file/src/main/scala/dev/rudiments/file/About.scala index e8f05534..d7370471 100644 --- a/file/src/main/scala/dev/rudiments/file/Header.scala +++ b/file/src/main/scala/dev/rudiments/file/About.scala @@ -7,34 +7,28 @@ import java.nio.charset.StandardCharsets.UTF_8 import scala.collection.immutable.ArraySeq -type Barr = Array[Byte] - -case class Header( - name: String, - fileType: Header.Type, +case class About( + fileType: About.Type, size: Int, checksum: SHA3 ) { - def toByteArray: Barr = { - val nameBytes = name.getBytes(UTF_8) - ByteBuffer.allocate(nameBytes.length + 1 + 32 + 4) + def toByteArray: Array[Byte] = { + ByteBuffer.allocate(1 + 32 + 4) .put(checksum.asArray) .put((fileType.ordinal + 1).toByte) .putInt(size) - .put(nameBytes) .array() } } -object Header { - def apply(data: Barr): Header = { +object About { + def apply(data: Array[Byte]): About = { val buff = ByteBuffer.wrap(data) - val hash = new Barr(32) + val hash = new Array[Byte](32) buff.get(hash) - val fileType = Header.Type(buff.get()) + val fileType = About.Type(buff.get()) val size = buff.getInt - val name = buff.asCharBuffer().toString - new Header(name, fileType, size, new SHA3(ArraySeq.unsafeWrapArray(hash))) + new About(fileType, size, new SHA3(ArraySeq.unsafeWrapArray(hash))) } enum Type: diff --git a/file/src/main/scala/dev/rudiments/file/File.scala b/file/src/main/scala/dev/rudiments/file/File.scala deleted file mode 100644 index 8db091aa..00000000 --- a/file/src/main/scala/dev/rudiments/file/File.scala +++ /dev/null @@ -1,62 +0,0 @@ -package dev.rudiments.file - -import dev.rudiments.utils.SHA3 - -import java.io.File -import java.nio.ByteBuffer -import java.nio.file.{Files, Path} -import java.nio.charset.StandardCharsets.UTF_8 - - -sealed trait Files(val header: Header) {} -object Files { - def apply(path: Path): Files = { - val file = path.toFile - if(file.isDirectory) { - Many.apply(file.getName, file.listFiles().toList) - } else { - One.apply(file.getName, java.nio.file.Files.readAllBytes(file.toPath)) - } - } -} - -case class One(override val header: Header, content: Barr) extends Files(header) { - override def equals(obj: Any): Boolean = obj match - case One(h, c) => this.header.equals(h) && this.content.sameElements(c) - case _ => false -} -object One { - def apply(path: Path): One = { - val file = path.toFile - if(file.isFile) { - One.apply(file.getName, java.nio.file.Files.readAllBytes(file.toPath)) - } else { - throw new IllegalArgumentException("Not a file") - } - } - - def apply(name: String, content: Barr): One = new One(Header(name, Header.Type.File, content.length, SHA3(content)), content) -} - -case class Many(self: Header, items: Seq[Header]) extends Files(self) {} -object Many { - def apply(path: Path): Many = { - val file = path.toFile - if (file.isDirectory) { - Many.apply(file.getName, file.listFiles().toList) - } else { - throw new IllegalArgumentException("Not a directory") - } - } - - def apply(name: String, files: List[File]): Many = { - val readen = files.map(f => Files(f.toPath).header) - val size = readen.foldLeft(0){ (acc, i) => acc + i.size } - val bytes = readen.foldLeft(Array.empty[Byte]){ (acc, i) => acc ++ i.toByteArray} - new Many( - new Header(name, Header.Type.Dir, size, SHA3(bytes)), - readen - ) - } -} - diff --git a/file/src/main/scala/dev/rudiments/file/Repository.scala b/file/src/main/scala/dev/rudiments/file/Repository.scala new file mode 100644 index 00000000..86bbac6e --- /dev/null +++ b/file/src/main/scala/dev/rudiments/file/Repository.scala @@ -0,0 +1,65 @@ +package dev.rudiments.file + +import dev.rudiments.utils.BytesCodec +import dev.rudiments.utils.SHA3 + +import java.io.File +import java.lang +import java.nio.ByteBuffer +import java.nio.file.Path +import scala.collection.immutable.ArraySeq +import scala.collection.mutable + +class Repository(path: Path) { + private val dir = path.toAbsolutePath + + val state: mutable.Map[Seq[String], FileData] = mutable.Map.empty + + def read(): Unit = { + val file = dir.toFile + val readen: FileData = if(file.isFile) { + readFileUnsafe(file) + } else if(file.isDirectory) { + readDirUnsafe(file, Seq.empty) + } else { + throw new IllegalArgumentException("Not a file or dir") + } + state.put(Seq.empty, readen) + } + + private def readDirUnsafe(file: File, prefix: Seq[String]): Dir = { + val content = file.listFiles().toList.sortBy(_.getName).map { + case f if f.isFile => f.getName -> readFileUnsafe(f) + case f if f.isDirectory => f.getName -> readDirUnsafe(f, prefix :+ f.getName) + } + + content.foreach((k, v) => state.put(prefix :+ k, v)) + + val (dirs, blobs) = content.partitionMap { + case (s, _: Dir) => Left(s) + case (s, _: Blob) => Right(s) + } + Dir(dirs, blobs) + } + + private def readFileUnsafe(file: File): Blob = { + Blob(java.nio.file.Files.readAllBytes(file.toPath)) + } +} + +sealed trait FileData { + def about: About +} + +import dev.rudiments.file.About.Type +case class Dir(directions: Seq[String], files: Seq[String]) extends FileData { + lazy val data: Array[Byte] = BytesCodec.encodeStrings(directions) ++ BytesCodec.encodeStrings(files) + + override def about: About = About(Type.Dir, data.length, SHA3(data)) +} +case class Blob(data: Seq[Byte]) extends FileData { + override def about: About = About(Type.File, data.size, SHA3(data.toArray[Byte])) +} +object Blob { + def apply(data: Array[Byte]): Blob = new Blob(ArraySeq.unsafeWrapArray(data)) +} diff --git a/file/src/test/scala/test/dev/rudiments/file/FileTest.scala b/file/src/test/scala/test/dev/rudiments/file/FileTest.scala index 8045d740..9ebcc344 100644 --- a/file/src/test/scala/test/dev/rudiments/file/FileTest.scala +++ b/file/src/test/scala/test/dev/rudiments/file/FileTest.scala @@ -1,6 +1,6 @@ package test.dev.rudiments.file -import dev.rudiments.file.* +import dev.rudiments.file.{Repository, *} import dev.rudiments.utils.SHA3 import org.junit.runner.RunWith import org.scalatest.matchers.should.Matchers @@ -13,29 +13,18 @@ import java.nio.file.Path @RunWith(classOf[JUnitRunner]) class FileTest extends AnyWordSpec with Matchers { - private val dir = Path.of("..", "file", "src", "test", "resources").toAbsolutePath - - private val f1 = new Header("1.txt", Header.Type.File, 10, SHA3("first file".getBytes(UTF_8))) - private val f2 = new Header("2.txt", Header.Type.File,11, SHA3("second file".getBytes(UTF_8))) - private val nested = new Header("nested", Header.Type.Dir, 11, SHA3(f2.toByteArray)) - - "can read single file" in { - Files.apply(dir.resolve(Path.of("example", "1.txt"))) should be( - new One(f1, "first file".getBytes(UTF_8)) - ) - } - - "can read directory with file" in { - Files.apply(dir.resolve(Path.of("example", "nested"))) should be ( - new Many(nested, Seq(f2)) - ) - } - - "can read nested directory" in { - Files.apply(dir.resolve("example")) should be( - new Many( - new Header("example", Header.Type.Dir, 21, SHA3(f1.toByteArray ++ nested.toByteArray)), - Seq(f1, nested) + private val dir = Path.of("..", "file", "src", "test", "resources", "example").toAbsolutePath + val repo = new Repository(dir) + + "can read repository" in { + repo.read() + repo.state.size should be (4) + repo.state.toMap should be ( + Map[Seq[String], FileData]( + Seq.empty -> Dir(Seq("nested"), Seq("1.txt")), + Seq("nested") -> Dir(Seq.empty, Seq("2.txt")), + Seq("nested", "2.txt") -> Blob("second file".getBytes(UTF_8)), + Seq("1.txt") -> Blob("first file".getBytes(UTF_8)) ) ) } diff --git a/git/build.gradle b/git/build.gradle index ce428f3b..869493d0 100644 --- a/git/build.gradle +++ b/git/build.gradle @@ -1,4 +1,5 @@ dependencies { implementation project(':core') + implementation project(':codec') implementation project(':file') } From ef9e75e624dc021a93f0ae091e9fa6fa347aeb35 Mon Sep 17 00:00:00 2001 From: gennady Date: Mon, 24 Apr 2023 02:21:34 +0600 Subject: [PATCH 23/75] add Tx and log with hashes --- .../scala/dev/rudiments/file/Repository.scala | 14 ++++++-- .../main/scala/dev/rudiments/file/Tx.scala | 36 +++++++++++++++++++ .../test/dev/rudiments/file/FileTest.scala | 12 +++++++ 3 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 file/src/main/scala/dev/rudiments/file/Tx.scala diff --git a/file/src/main/scala/dev/rudiments/file/Repository.scala b/file/src/main/scala/dev/rudiments/file/Repository.scala index 86bbac6e..43d0a696 100644 --- a/file/src/main/scala/dev/rudiments/file/Repository.scala +++ b/file/src/main/scala/dev/rudiments/file/Repository.scala @@ -14,8 +14,10 @@ class Repository(path: Path) { private val dir = path.toAbsolutePath val state: mutable.Map[Seq[String], FileData] = mutable.Map.empty + val log: mutable.Buffer[Commit] = mutable.Buffer.empty def read(): Unit = { + given tx: Tx = new Tx(state.toMap) val file = dir.toFile val readen: FileData = if(file.isFile) { readFileUnsafe(file) @@ -24,16 +26,22 @@ class Repository(path: Path) { } else { throw new IllegalArgumentException("Not a file or dir") } - state.put(Seq.empty, readen) + tx.put(Seq.empty, readen) + + val commit = tx.makeCommit + if(commit.changes.nonEmpty) { + log.append(commit) + state ++= commit.changed + } } - private def readDirUnsafe(file: File, prefix: Seq[String]): Dir = { + private def readDirUnsafe(file: File, prefix: Seq[String])(using tx: Tx): Dir = { val content = file.listFiles().toList.sortBy(_.getName).map { case f if f.isFile => f.getName -> readFileUnsafe(f) case f if f.isDirectory => f.getName -> readDirUnsafe(f, prefix :+ f.getName) } - content.foreach((k, v) => state.put(prefix :+ k, v)) + content.foreach((k, v) => tx.put(prefix :+ k, v)) val (dirs, blobs) = content.partitionMap { case (s, _: Dir) => Left(s) diff --git a/file/src/main/scala/dev/rudiments/file/Tx.scala b/file/src/main/scala/dev/rudiments/file/Tx.scala new file mode 100644 index 00000000..2013965f --- /dev/null +++ b/file/src/main/scala/dev/rudiments/file/Tx.scala @@ -0,0 +1,36 @@ +package dev.rudiments.file + +import dev.rudiments.utils.SHA3 + +import scala.collection.mutable + +class Tx(val initial: Map[Seq[String], FileData]) { + val changed: mutable.Map[Seq[String], FileData] = mutable.Map.empty + val log: mutable.Map[Seq[String], FileLog] = mutable.Map.empty + + def put(k: Seq[String], v: FileData): Unit = { + initial.get(k) match //TODO drop when was in changed but not anymore + case Some(found) => + if (found.about != v.about) { + changed.put(k, v) + log.put(k, FileLog(Some(found.about.checksum), Some(v.about.checksum))) + } + case None => + changed.put(k, v) + log.put(k, FileLog(None, Some(v.about.checksum))) + } + + def makeCommit: Commit = Commit(changed.toMap.map((k,change) => k -> (log(k), change))) +} + + +case class FileLog( + before: Option[SHA3], + after: Option[SHA3] +) + +case class Commit( + changes: Map[Seq[String], (FileLog, FileData)] +) { + def changed: Map[Seq[String], FileData] = changes.map { case (k, (_, change)) => k -> change } +} \ No newline at end of file diff --git a/file/src/test/scala/test/dev/rudiments/file/FileTest.scala b/file/src/test/scala/test/dev/rudiments/file/FileTest.scala index 9ebcc344..5f8787b4 100644 --- a/file/src/test/scala/test/dev/rudiments/file/FileTest.scala +++ b/file/src/test/scala/test/dev/rudiments/file/FileTest.scala @@ -28,4 +28,16 @@ class FileTest extends AnyWordSpec with Matchers { ) ) } + + "can read hole project" ignore { + val r = new Repository(Path.of(".")) + r.read() + r.log.size should be (1) + } + + "can read git project" ignore { + val r = new Repository(Path.of("../git")) + r.read() + r.log.size should be(1) + } } From 3a69ba0c66026e0f05306705514385e0d03e6ecc Mon Sep 17 00:00:00 2001 From: gennady Date: Mon, 24 Apr 2023 03:20:03 +0600 Subject: [PATCH 24/75] deleting draft --- .../scala/dev/rudiments/utils/Hashed.scala | 2 ++ .../scala/dev/rudiments/file/Repository.scala | 4 ++++ .../main/scala/dev/rudiments/file/Tx.scala | 20 +++++++++++++++++-- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/dev/rudiments/utils/Hashed.scala b/core/src/main/scala/dev/rudiments/utils/Hashed.scala index f609a0b4..24e5fbf2 100644 --- a/core/src/main/scala/dev/rudiments/utils/Hashed.scala +++ b/core/src/main/scala/dev/rudiments/utils/Hashed.scala @@ -54,4 +54,6 @@ object SHA3 { 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/file/src/main/scala/dev/rudiments/file/Repository.scala b/file/src/main/scala/dev/rudiments/file/Repository.scala index 43d0a696..f8356331 100644 --- a/file/src/main/scala/dev/rudiments/file/Repository.scala +++ b/file/src/main/scala/dev/rudiments/file/Repository.scala @@ -71,3 +71,7 @@ case class Blob(data: Seq[Byte]) extends FileData { object Blob { def apply(data: Array[Byte]): Blob = new Blob(ArraySeq.unsafeWrapArray(data)) } + +case object NotExist extends FileData { + override def about: About = About(Type.File, 0, SHA3.empty) +} \ No newline at end of file diff --git a/file/src/main/scala/dev/rudiments/file/Tx.scala b/file/src/main/scala/dev/rudiments/file/Tx.scala index 2013965f..1c9988d1 100644 --- a/file/src/main/scala/dev/rudiments/file/Tx.scala +++ b/file/src/main/scala/dev/rudiments/file/Tx.scala @@ -9,11 +9,14 @@ class Tx(val initial: Map[Seq[String], FileData]) { val log: mutable.Map[Seq[String], FileLog] = mutable.Map.empty def put(k: Seq[String], v: FileData): Unit = { - initial.get(k) match //TODO drop when was in changed but not anymore + initial.get(k) match case Some(found) => - if (found.about != v.about) { + if (found.about != v.about) { //TODO Dir change => delete files changed.put(k, v) log.put(k, FileLog(Some(found.about.checksum), Some(v.about.checksum))) + } else { + changed.remove(k) + log.remove(k) } case None => changed.put(k, v) @@ -21,6 +24,19 @@ class Tx(val initial: Map[Seq[String], FileData]) { } def makeCommit: Commit = Commit(changed.toMap.map((k,change) => k -> (log(k), change))) + + def deleting(from: Seq[String]): Unit = { + initial.get(from) match { + case Some(d@Dir(files, dirs)) => + changed.put(from, NotExist) + log.put(from, FileLog(None, Some(d.about.checksum))) + dirs.foreach(s => deleting(from :+ s)) + files.foreach(s => deleting(from :+ s)) + case Some(b: Blob) => + changed.put(from, NotExist) + log.put(from, FileLog(None, Some(b.about.checksum))) + } + } } From 1b48bddc216c107a70d47f865f9032bcf1012bf6 Mon Sep 17 00:00:00 2001 From: gennady Date: Mon, 24 Apr 2023 03:29:32 +0600 Subject: [PATCH 25/75] improve types --- .../scala/dev/rudiments/file/Repository.scala | 10 +++++++--- file/src/main/scala/dev/rudiments/file/Tx.scala | 15 ++++++++------- .../scala/test/dev/rudiments/file/FileTest.scala | 4 ++-- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/file/src/main/scala/dev/rudiments/file/Repository.scala b/file/src/main/scala/dev/rudiments/file/Repository.scala index f8356331..088a27ec 100644 --- a/file/src/main/scala/dev/rudiments/file/Repository.scala +++ b/file/src/main/scala/dev/rudiments/file/Repository.scala @@ -13,8 +13,10 @@ import scala.collection.mutable class Repository(path: Path) { private val dir = path.toAbsolutePath - val state: mutable.Map[Seq[String], FileData] = mutable.Map.empty + val state: mutable.Map[Rel, FileData] = mutable.Map.empty val log: mutable.Buffer[Commit] = mutable.Buffer.empty + + val ignored: Set[Rel] = Set.empty def read(): Unit = { given tx: Tx = new Tx(state.toMap) @@ -35,7 +37,7 @@ class Repository(path: Path) { } } - private def readDirUnsafe(file: File, prefix: Seq[String])(using tx: Tx): Dir = { + private def readDirUnsafe(file: File, prefix: Rel)(using tx: Tx): Dir = { val content = file.listFiles().toList.sortBy(_.getName).map { case f if f.isFile => f.getName -> readFileUnsafe(f) case f if f.isDirectory => f.getName -> readDirUnsafe(f, prefix :+ f.getName) @@ -55,12 +57,14 @@ class Repository(path: Path) { } } +type Rel = Seq[String] + sealed trait FileData { def about: About } import dev.rudiments.file.About.Type -case class Dir(directions: Seq[String], files: Seq[String]) extends FileData { +case class Dir(directions: Rel, files: Rel) extends FileData { lazy val data: Array[Byte] = BytesCodec.encodeStrings(directions) ++ BytesCodec.encodeStrings(files) override def about: About = About(Type.Dir, data.length, SHA3(data)) diff --git a/file/src/main/scala/dev/rudiments/file/Tx.scala b/file/src/main/scala/dev/rudiments/file/Tx.scala index 1c9988d1..0e67d3ca 100644 --- a/file/src/main/scala/dev/rudiments/file/Tx.scala +++ b/file/src/main/scala/dev/rudiments/file/Tx.scala @@ -4,11 +4,11 @@ import dev.rudiments.utils.SHA3 import scala.collection.mutable -class Tx(val initial: Map[Seq[String], FileData]) { - val changed: mutable.Map[Seq[String], FileData] = mutable.Map.empty - val log: mutable.Map[Seq[String], FileLog] = mutable.Map.empty +class Tx(val initial: Map[Rel, FileData]) { + val changed: mutable.Map[Rel, FileData] = mutable.Map.empty + val log: mutable.Map[Rel, FileLog] = mutable.Map.empty - def put(k: Seq[String], v: FileData): Unit = { + def put(k: Rel, v: FileData): Unit = { initial.get(k) match case Some(found) => if (found.about != v.about) { //TODO Dir change => delete files @@ -25,7 +25,7 @@ class Tx(val initial: Map[Seq[String], FileData]) { def makeCommit: Commit = Commit(changed.toMap.map((k,change) => k -> (log(k), change))) - def deleting(from: Seq[String]): Unit = { + def deleting(from: Rel): Unit = { initial.get(from) match { case Some(d@Dir(files, dirs)) => changed.put(from, NotExist) @@ -35,6 +35,7 @@ class Tx(val initial: Map[Seq[String], FileData]) { case Some(b: Blob) => changed.put(from, NotExist) log.put(from, FileLog(None, Some(b.about.checksum))) + case _ => //DO nothing } } } @@ -46,7 +47,7 @@ case class FileLog( ) case class Commit( - changes: Map[Seq[String], (FileLog, FileData)] + changes: Map[Rel, (FileLog, FileData)] ) { - def changed: Map[Seq[String], FileData] = changes.map { case (k, (_, change)) => k -> change } + def changed: Map[Rel, FileData] = changes.map { case (k, (_, change)) => k -> change } } \ No newline at end of file diff --git a/file/src/test/scala/test/dev/rudiments/file/FileTest.scala b/file/src/test/scala/test/dev/rudiments/file/FileTest.scala index 5f8787b4..884ffd92 100644 --- a/file/src/test/scala/test/dev/rudiments/file/FileTest.scala +++ b/file/src/test/scala/test/dev/rudiments/file/FileTest.scala @@ -1,6 +1,6 @@ package test.dev.rudiments.file -import dev.rudiments.file.{Repository, *} +import dev.rudiments.file.* import dev.rudiments.utils.SHA3 import org.junit.runner.RunWith import org.scalatest.matchers.should.Matchers @@ -20,7 +20,7 @@ class FileTest extends AnyWordSpec with Matchers { repo.read() repo.state.size should be (4) repo.state.toMap should be ( - Map[Seq[String], FileData]( + Map[Rel, FileData]( Seq.empty -> Dir(Seq("nested"), Seq("1.txt")), Seq("nested") -> Dir(Seq.empty, Seq("2.txt")), Seq("nested", "2.txt") -> Blob("second file".getBytes(UTF_8)), From c2c663d5c484313eff5ffa636785cdd9cc9e8167 Mon Sep 17 00:00:00 2001 From: gennady Date: Tue, 25 Apr 2023 11:37:06 +0600 Subject: [PATCH 26/75] delete nested when dir updating --- file/src/main/scala/dev/rudiments/file/Tx.scala | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/file/src/main/scala/dev/rudiments/file/Tx.scala b/file/src/main/scala/dev/rudiments/file/Tx.scala index 0e67d3ca..3f7f3eb6 100644 --- a/file/src/main/scala/dev/rudiments/file/Tx.scala +++ b/file/src/main/scala/dev/rudiments/file/Tx.scala @@ -11,9 +11,19 @@ class Tx(val initial: Map[Rel, FileData]) { def put(k: Rel, v: FileData): Unit = { initial.get(k) match case Some(found) => - if (found.about != v.about) { //TODO Dir change => delete files + if (found.about != v.about) { changed.put(k, v) log.put(k, FileLog(Some(found.about.checksum), Some(v.about.checksum))) + + (found, v) match { + case (Dir(d1, f1), Dir(d2, f2)) => + (d1.toSet -- d2.toSet).foreach { s => deleting(k :+ s) } + (f1.toSet -- f2.toSet).foreach { s => deleting(k :+ s) } + case (Dir(d, f), _) => + d.foreach { s => deleting(k :+ s) } + f.foreach { s => deleting(k :+ s) } + case _ => //do nothing + } } else { changed.remove(k) log.remove(k) @@ -25,7 +35,7 @@ class Tx(val initial: Map[Rel, FileData]) { def makeCommit: Commit = Commit(changed.toMap.map((k,change) => k -> (log(k), change))) - def deleting(from: Rel): Unit = { + def deleting(from: Rel): Unit = { //TODO replace with Delete on Dir instead of Repo initial.get(from) match { case Some(d@Dir(files, dirs)) => changed.put(from, NotExist) From e8acdb49b79842d47478732ef2e0511ddb99996d Mon Sep 17 00:00:00 2001 From: gennady Date: Tue, 25 Apr 2023 11:47:34 +0600 Subject: [PATCH 27/75] add ignore list --- .../main/scala/dev/rudiments/file/Repository.scala | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/file/src/main/scala/dev/rudiments/file/Repository.scala b/file/src/main/scala/dev/rudiments/file/Repository.scala index 088a27ec..481837d3 100644 --- a/file/src/main/scala/dev/rudiments/file/Repository.scala +++ b/file/src/main/scala/dev/rudiments/file/Repository.scala @@ -16,7 +16,7 @@ class Repository(path: Path) { val state: mutable.Map[Rel, FileData] = mutable.Map.empty val log: mutable.Buffer[Commit] = mutable.Buffer.empty - val ignored: Set[Rel] = Set.empty + val ignored: Set[Rel] = Set(Seq(".git"), Seq(".gradle"), Seq(".idea")) def read(): Unit = { given tx: Tx = new Tx(state.toMap) @@ -38,10 +38,12 @@ class Repository(path: Path) { } private def readDirUnsafe(file: File, prefix: Rel)(using tx: Tx): Dir = { - val content = file.listFiles().toList.sortBy(_.getName).map { - case f if f.isFile => f.getName -> readFileUnsafe(f) - case f if f.isDirectory => f.getName -> readDirUnsafe(f, prefix :+ f.getName) - } + val content = file.listFiles().toList.sortBy(_.getName) + .filterNot(f => ignored.contains(prefix :+ f.getName)) + .map { + case f if f.isFile => f.getName -> readFileUnsafe(f) + case f if f.isDirectory => f.getName -> readDirUnsafe(f, prefix :+ f.getName) + } content.foreach((k, v) => tx.put(prefix :+ k, v)) From 074c6935f98bc23af5d1a63846d6b03dca771452 Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 27 Apr 2023 00:45:48 +0600 Subject: [PATCH 28/75] diff util --- core/build.gradle | 2 +- .../main/scala/dev/rudiments/utils/Diff.scala | 95 +++++++++++++++++++ .../test/dev/rudiments/utils/DiffTest.scala | 57 +++++++++++ 3 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 core/src/main/scala/dev/rudiments/utils/Diff.scala create mode 100644 core/src/test/scala/test/dev/rudiments/utils/DiffTest.scala diff --git a/core/build.gradle b/core/build.gradle index 0ce6a166..cf22ba9f 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -1,3 +1,3 @@ dependencies { - + implementation 'io.github.java-diff-utils:java-diff-utils:4.12' } \ No newline at end of file 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/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..2e982c31 --- /dev/null +++ b/core/src/test/scala/test/dev/rudiments/utils/DiffTest.scala @@ -0,0 +1,57 @@ +package test.dev.rudiments.utils + +import com.github.difflib.DiffUtils +import dev.rudiments.utils.{Chunk, Delta, Diff, Unified} +import org.junit.runner.RunWith +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import org.scalatestplus.junit.JUnitRunner + +import scala.jdk.CollectionConverters.* + +@RunWith(classOf[JUnitRunner]) +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 + ) + )) + ) + } +} From da34535dbfd1871d869fcd9ab4cf699264e55099 Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 27 Apr 2023 01:12:30 +0600 Subject: [PATCH 29/75] drop crashing codec module --- codec/build.gradle | 6 ------ example/build.gradle | 1 - file/build.gradle | 1 - git/build.gradle | 1 - settings.gradle | 1 - 5 files changed, 10 deletions(-) delete mode 100644 codec/build.gradle diff --git a/codec/build.gradle b/codec/build.gradle deleted file mode 100644 index ea8c1f25..00000000 --- a/codec/build.gradle +++ /dev/null @@ -1,6 +0,0 @@ -dependencies { - implementation project(':core') - - implementation 'io.circe:circe-core_3:0.14.5' - implementation 'io.circe:circe-generic_3:0.14.5' -} \ No newline at end of file diff --git a/example/build.gradle b/example/build.gradle index e9ea93db..d9e2ff40 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -1,6 +1,5 @@ dependencies { implementation project(':core') - implementation project(':codec') implementation 'ch.qos.logback:logback-classic:1.4.5' } diff --git a/file/build.gradle b/file/build.gradle index f2dd804a..e4dbb7fe 100644 --- a/file/build.gradle +++ b/file/build.gradle @@ -1,4 +1,3 @@ dependencies { implementation project(':core') - implementation project(':codec') } diff --git a/git/build.gradle b/git/build.gradle index 869493d0..ce428f3b 100644 --- a/git/build.gradle +++ b/git/build.gradle @@ -1,5 +1,4 @@ dependencies { implementation project(':core') - implementation project(':codec') implementation project(':file') } diff --git a/settings.gradle b/settings.gradle index 8d83ea9a..0c00cb08 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,5 @@ rootProject.name = 'hardcore' include 'core' -include 'codec' include 'file' include 'git' include 'example' From 45d8f0a583818fcea0d888ff8bd34e3ec9d1bd6a Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 27 Apr 2023 02:15:15 +0600 Subject: [PATCH 30/75] reset state when removing non-existent files --- file/src/main/scala/dev/rudiments/file/Tx.scala | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/file/src/main/scala/dev/rudiments/file/Tx.scala b/file/src/main/scala/dev/rudiments/file/Tx.scala index 3f7f3eb6..00c501cc 100644 --- a/file/src/main/scala/dev/rudiments/file/Tx.scala +++ b/file/src/main/scala/dev/rudiments/file/Tx.scala @@ -29,8 +29,14 @@ class Tx(val initial: Map[Rel, FileData]) { log.remove(k) } case None => - changed.put(k, v) - log.put(k, FileLog(None, Some(v.about.checksum))) + v match { + case NotExist => + changed.remove(k) + log.remove(k) + case _ => + changed.put(k, v) + log.put(k, FileLog(None, Some(v.about.checksum))) + } } def makeCommit: Commit = Commit(changed.toMap.map((k,change) => k -> (log(k), change))) From 7f994f4c3bcfe169e14ff1bdb440ac8d7bb4a27d Mon Sep 17 00:00:00 2001 From: gennady Date: Sat, 29 Apr 2023 20:16:37 +0600 Subject: [PATCH 31/75] improve error handling basing on git project repo --- .../scala/dev/rudiments/git/GitObject.scala | 3 +- .../scala/dev/rudiments/git/Repository.scala | 49 ++++++++++++++----- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/git/src/main/scala/dev/rudiments/git/GitObject.scala b/git/src/main/scala/dev/rudiments/git/GitObject.scala index f34d7c03..8fbfa3fb 100644 --- a/git/src/main/scala/dev/rudiments/git/GitObject.scala +++ b/git/src/main/scala/dev/rudiments/git/GitObject.scala @@ -64,6 +64,7 @@ object Tree { enum Mode(val code: String): case File extends Mode("100644") + case GroupFile extends Mode("100664") //TODO merge with File 100644? case Executable extends Mode("100755") case SymbolicLink extends Mode("120000") case SubTree extends Mode("40000") @@ -196,7 +197,7 @@ object Commit { m.group(13) ) }.getOrElse { - throw new IllegalArgumentException(s"Can't read commit: $asString") + throw new IllegalArgumentException(s"Can't read commit: ${asString.take(50)}") } } } diff --git a/git/src/main/scala/dev/rudiments/git/Repository.scala b/git/src/main/scala/dev/rudiments/git/Repository.scala index 1a0c87ad..1c37845e 100644 --- a/git/src/main/scala/dev/rudiments/git/Repository.scala +++ b/git/src/main/scala/dev/rudiments/git/Repository.scala @@ -17,6 +17,8 @@ class Repository(root: Path) extends Log { val tags: mutable.Buffer[String] = mutable.Buffer.empty val objects: mutable.Map[SHA1, GitObject] = mutable.HashMap.empty + val errors: mutable.Map[SHA1, (Entry, String)] = mutable.HashMap.empty + private val packPath = root.resolve(Path.of(".git", "objects", "pack")) def read(): Unit = { @@ -33,20 +35,41 @@ class Repository(root: Path) extends Log { } } - def readPack(hash: String): Unit = Pack.readPack(root, hash).objects.foreach { - case (key, Entry(PackObj.Tree, _, data, _, _)) => - objects.put(key, Tree(ZLib.unpack(data))) - case (key, Entry(PackObj.Commit, _, data, _, _)) => - try { - objects.put(key, Commit(ZLib.unpack(data))) - } catch { - case e: Exception => log.error("Failed commit {}", key) - } + 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, _, data, _, _)) => + try { + objects.put(key, Tree(ZLib.unpack(data))) + } catch { + case e: Exception => errors.put(key, (v, e.getLocalizedMessage)) + } + case (key, v@Entry(PackObj.Commit, _, data, _, _)) => + try { + objects.put(key, Commit(ZLib.unpack(data))) + } catch { + case e: Exception => errors.put(key, (v, e.getLocalizedMessage)) + } - case (key, Entry(PackObj.Blob, _, data, _, _)) => - objects.put(key, Blob(ZLib.unpack(data))) + case (key, v@Entry(PackObj.Blob, _, data, _, _)) => + try { + objects.put(key, Blob(ZLib.unpack(data))) + } catch { + case e: Exception => errors.put(key, (v, e.getLocalizedMessage)) + } - case (key, Entry(PackObj.Tag, _, _, _, _)) => // TODO - case (key, entry) => // filtered + case (key, v@Entry(PackObj.Tag, _, _, _, _)) => errors.put(key, (v, "Tag parse are not implemented")) + case (key, entry) => errors.put(key, (entry, "Parse are not implemented")) + } + + 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) + } + log.info(s"Parsed ${packEntries.size} entries into ${objects.size - initialObjects} objects") } } From f70bdf188b9838f3ef8364dbec18efe9b22a364d Mon Sep 17 00:00:00 2001 From: gennady Date: Sat, 29 Apr 2023 20:51:54 +0600 Subject: [PATCH 32/75] fix tree parsing --- git/src/main/scala/dev/rudiments/git/GitObject.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git/src/main/scala/dev/rudiments/git/GitObject.scala b/git/src/main/scala/dev/rudiments/git/GitObject.scala index 8fbfa3fb..227dab42 100644 --- a/git/src/main/scala/dev/rudiments/git/GitObject.scala +++ b/git/src/main/scala/dev/rudiments/git/GitObject.scala @@ -53,7 +53,7 @@ object Tree { 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 :: Nil => items.addOne(Item(Mode(mode), name, new SHA1(data.slice(div + 1, div + 21)))) + case mode :: name => items.addOne(Item(Mode(mode), name.mkString(" "), new SHA1(data.slice(div + 1, div + 21)))) case other => throw new IllegalArgumentException(s"Doesn't look like a tree item: `${other.mkString(";")}`") } start = div + 21 @@ -64,7 +64,7 @@ object Tree { enum Mode(val code: String): case File extends Mode("100644") - case GroupFile extends Mode("100664") //TODO merge with File 100644? + case GroupFile extends Mode("100664") case Executable extends Mode("100755") case SymbolicLink extends Mode("120000") case SubTree extends Mode("40000") From eeaf67c2485e378bb834f7e6c7720944b5fa0eb0 Mon Sep 17 00:00:00 2001 From: gennady Date: Sun, 30 Apr 2023 01:11:30 +0600 Subject: [PATCH 33/75] improve commit and it's parsing --- .../scala/dev/rudiments/git/GitObject.scala | 171 +++++------------- .../scala/dev/rudiments/git/Repository.scala | 2 +- git/src/main/sw.sc | 47 +++++ .../dev/rudiments/git/GitCommitsTest.scala | 4 +- .../dev/rudiments/git/GitObjectTest.scala | 2 +- 5 files changed, 99 insertions(+), 127 deletions(-) create mode 100644 git/src/main/sw.sc diff --git a/git/src/main/scala/dev/rudiments/git/GitObject.scala b/git/src/main/scala/dev/rudiments/git/GitObject.scala index 227dab42..c8537d16 100644 --- a/git/src/main/scala/dev/rudiments/git/GitObject.scala +++ b/git/src/main/scala/dev/rudiments/git/GitObject.scala @@ -1,5 +1,6 @@ package dev.rudiments.git +import dev.rudiments.git.Commit.Field.{Author, Parent} import dev.rudiments.utils.{Hashed, SHA1, ZLib} import java.lang @@ -34,7 +35,6 @@ sealed trait GitObject(kind: String) { final case class Blob(content: String) extends GitObject("blob") { override def data: Array[Byte] = content.getBytes(UTF_8) } - object Blob { def apply(data: Array[Byte]): Blob = new Blob(new String(data, UTF_8)) } @@ -42,7 +42,6 @@ object Blob { 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") @@ -85,125 +84,56 @@ object Tree { final case class Commit( tree: SHA1, - parent: Option[SHA1], + parent: Seq[SHA1], author: Commit.AuthRecord, committer: Commit.AuthRecord, - message: String + message: String, + signature: Option[String] = None, + originalData: Option[String] = None ) extends GitObject("commit") { override def data: Array[Byte] = { - s"""tree $tree - |parent ${parent.map(_.toString).getOrElse("NIL")} - |author $author - |committer $committer - | - |$message""".stripMargin - .getBytes(UTF_8) + 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.getBytes(UTF_8) } } - object Commit { - val signedPattern: Regex = - """tree (\w{40}) - |parent (\w{40}) - |author (.+) <(.+)> (\d{10,20}) (.+) - |committer (.+) <(.+)> (\d{10,20}) (.+) - |gpgsig -----BEGIN PGP SIGNATURE----- - |\s* - | .{64} - | .{64} - | .{64} - | .{64} - | .{64} - | .{64} - | .{1,64} - | -----END PGP SIGNATURE----- - |\s* - |\s* - |(.*)""".stripMargin.r - - val signed2Pattern: Regex = - """tree (\w{40}) - |parent (\w{40}) - |(parent (\w{40}))? - |author (.+) <(.+)> (\d{10,20}) (.+) - |committer (.+) <(.+)> (\d{10,20}) (.+) - |gpgsig -----BEGIN PGP SIGNATURE----- - |\s* - | .{64} - | .{64} - | .{64} - | .{64} - | .{64} - | .{64} - | .{1,64} - | -----END PGP SIGNATURE----- - |\s* - |\s* - |(.*)""".stripMargin.r - - val commitPattern: Regex = - """tree (\w{40}) - |parent (\w{40}) - |author (.+) <(.+)> (\d{10,20}) (.+) - |committer (.+) <(.+)> (\d{10,20}) (.+) - | - |(.*)""".stripMargin.r - - val mergePattern: Regex = - """tree (\w{40}) - |parent (\w{40}) - |(parent (\w{40}))? - |author (.+) <(.+)> (\d{10,20}) (.+) - |committer (.+) <(.+)> (\d{10,20}) (.+) - | - |(.*)""".stripMargin.r + 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 asString = new String(data, UTF_8) - try { - commitPattern.findFirstMatchIn(asString).map { m => - new Commit( - SHA1.fromHex(m.group(1)), - Some(SHA1.fromHex(m.group(2))), - AuthRecord(m.group(3), m.group(4), Instant.ofEpochMilli(m.group(5).toLong).atZone(ZoneId.of(m.group(6)))), - AuthRecord(m.group(7), m.group(8), Instant.ofEpochMilli(m.group(9).toLong).atZone(ZoneId.of(m.group(10)))), - m.group(11) - ) - }.getOrElse { - mergePattern.findFirstMatchIn(asString).map { m => - new Commit( - SHA1.fromHex(m.group(1)), - Some(SHA1.fromHex(m.group(2))), - AuthRecord(m.group(5), m.group(6), Instant.ofEpochMilli(m.group(7).toLong).atZone(ZoneId.of(m.group(8)))), - AuthRecord(m.group(9), m.group(10), Instant.ofEpochMilli(m.group(11).toLong).atZone(ZoneId.of(m.group(12)))), - m.group(13) - ) - }.getOrElse { - signedPattern.findFirstMatchIn(asString).map { m => - new Commit( - SHA1.fromHex(m.group(1)), - Some(SHA1.fromHex(m.group(2))), - AuthRecord(m.group(3), m.group(4), Instant.ofEpochMilli(m.group(5).toLong).atZone(ZoneId.of(m.group(6)))), - AuthRecord(m.group(7), m.group(8), Instant.ofEpochMilli(m.group(9).toLong).atZone(ZoneId.of(m.group(10)))), - m.group(11) - ) - }.getOrElse { - signed2Pattern.findFirstMatchIn(asString).map { m => - new Commit( - SHA1.fromHex(m.group(1)), - Some(SHA1.fromHex(m.group(2))), - AuthRecord(m.group(5), m.group(6), Instant.ofEpochMilli(m.group(7).toLong).atZone(ZoneId.of(m.group(8)))), - AuthRecord(m.group(9), m.group(10), Instant.ofEpochMilli(m.group(11).toLong).atZone(ZoneId.of(m.group(12)))), - m.group(13) - ) - }.getOrElse { - throw new IllegalArgumentException(s"Can't read commit: ${asString.take(50)}") - } - } - } - } - } catch { - case e: Exception => throw e + val str = new String(data, UTF_8) + val asMap = Field.values.toSeq.map { f => f -> f.regex.findAllMatchIn(str) }.toMap + val candidate = new Commit( + SHA1.fromHex(asMap(Field.Tree).map(_.group(1)).toSeq.head), + asMap(Parent).map(_.group(1)).toSeq.map(SHA1.fromHex), + asMap(Author).map(AuthRecord.apply).toSeq.head, + asMap(Field.Committer).map(AuthRecord.apply).toSeq.head, + asMap(Field.Message).mkString("\n") + ) + + if(new String(candidate.data, UTF_8) == str) { + candidate + } else { + candidate.copy(originalData = Some(str)) } } @@ -219,16 +149,11 @@ object Commit { } object AuthRecord { - def parse(s: String): AuthRecord = { - s.split(" ").toList match - case name :: mail :: time :: zone :: Nil => - val z = ZoneId.of(zone) - val when = Instant.ofEpochMilli(time.toLong).atZone(z) - new AuthRecord(name, mail, when) - case name1 :: name2 :: mail :: time :: zone :: Nil => - val z = ZoneId.of(zone) - val when = Instant.ofEpochMilli(time.toLong).atZone(z) - new AuthRecord(name1 + " " + name2, mail, when) - } + 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))) + ) } } \ No newline at end of file diff --git a/git/src/main/scala/dev/rudiments/git/Repository.scala b/git/src/main/scala/dev/rudiments/git/Repository.scala index 1c37845e..76a947db 100644 --- a/git/src/main/scala/dev/rudiments/git/Repository.scala +++ b/git/src/main/scala/dev/rudiments/git/Repository.scala @@ -68,7 +68,7 @@ class Repository(root: Path) extends Log { 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) + 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/sw.sc b/git/src/main/sw.sc new file mode 100644 index 00000000..0c081063 --- /dev/null +++ b/git/src/main/sw.sc @@ -0,0 +1,47 @@ +import java.nio.charset.StandardCharsets.UTF_8 +import dev.rudiments.git.Commit + +val hash = "81e30fc0c74ec502f99fd2f91784bfe1606e6a9f" + +val str = """tree af1dd2b5ea96f90a06bd8b965ff784898782d7d2 + |parent 55095d03a9272ad936d8cf6e23a46e4fcc1da98d + |author Yi-Jyun Pan 1628920089 +0800 + |committer Yi-Jyun Pan 1628920919 +0800 + |gpgsig -----BEGIN PGP SIGNATURE----- + | + | iQIzBAABCAAdFiEEs0GfOWTlygReNS0yQhVLGxz+M3cFAmEXXFkACgkQQhVLGxz+ + | M3d2Cw//R5H79nED3KRVmOoDNfpncopOf8m7dTc8Z+BIigtIVhQeIwnPih52u6tP + | susk9ZyjaDXFJ6bqUBXKuXcFmttiw1Yklr15rm79HFceZyzCFZpw/HqNm/Wnx8sE + | GYEoZ35tjuBVMog8zuRfXQNtfqwCLvUPk5xU0J1+OCM1TMkS5iLFOuvO0/x4kUNL + | ARRi5bIvZgrS4+BqXuTMk2L64Ev+R8WdAOvUFsgwgwlH++UnHUJkIEaf9kczLgr8 + | Dg0lcZGdMlTKDEgLQJ0BDrFZX0eoXA8/yi3sBvqQ/lIBszznxZViNcFMierVvPAz + | g1jWmyvo2s6MMmA/lVvS4/D1XyQPf5p09BhVBogqKk9U+2Rqun35QjtH2Wtb22AJ + | xBsFt3uZ3MgenTc9TF8yXe4DZK60XHqXDIrslAUvc9LQ2QL4dXE7FsSaQVi49mls + | 3nzfK5tEMU2cEjX7DYxhfSWsh1g7E+GA2fYelol1bhbPOmUcVk5FyGj42YVn7o3d + | xsdwWWYAePNuToeuoMXCdkIioQHoHx1MC4k6IzEK0lFJQ/998C1MPM/K8uMOkWdZ + | ag48tK54+0Nvek6ULAbWS9IOTyviaHtEoaUtVIfPDDhMhGekXW5OFEFV3eJwPJ/H + | XMwETjmmWoUFoOiLomtCxR2cihdjrE9OPsgseXMIJ1oLGxPc9sE= + | =14DB + | -----END PGP SIGNATURE----- + | + |l10n: zh_TW.po: update for v2.33.0 rnd 2 + | + |Signed-off-by: Yi-Jyun Pan + |""".stripMargin + + + +val treeR = """tree (?\w{40})""".r +val parentR = """parent (?\w{40})""".r +val authorR = """author (?.+) <(?.+)> (?\d{10,20}) (?.+)""".r +val committerR = """committer (?.+) <(?.+)> (?\d{10,20}) (?.+)""".r +val signatureR = """gpgsig -----BEGIN PGP SIGNATURE-----(.*\n)* -----END PGP SIGNATURE-----\n""".r + +val tree = treeR.findAllMatchIn(str).map(_.group(1)).toSeq.head +val parent = parentR.findAllMatchIn(str).map(_.group(1)).toSeq +val author = authorR.findAllMatchIn(str).map(_.group(1)).toSeq.head +val committer = committerR.findAllMatchIn(str).map(_.group(1)).toSeq.head + +val signature = signatureR.findAllMatchIn(str).mkString("|") + +val commit = Commit.apply(str.getBytes(UTF_8)) \ No newline at end of file diff --git a/git/src/test/scala/test/dev/rudiments/git/GitCommitsTest.scala b/git/src/test/scala/test/dev/rudiments/git/GitCommitsTest.scala index abdf3419..a05232a8 100644 --- a/git/src/test/scala/test/dev/rudiments/git/GitCommitsTest.scala +++ b/git/src/test/scala/test/dev/rudiments/git/GitCommitsTest.scala @@ -19,8 +19,8 @@ class GitCommitsTest extends AnyWordSpec with Matchers with Log { while(h != "SUCCESS" || h != "FAIL") { Reader.read(dir, h) match { - case Right(c: Commit) if c.parent.isDefined => - h = c.parent.get.toString + case Right(c: Commit) if c.parent.nonEmpty => + h = c.parent.head.toString i += 1 case Right(_) => h = "SUCCESS" case Left(err) => h = "FAIL" diff --git a/git/src/test/scala/test/dev/rudiments/git/GitObjectTest.scala b/git/src/test/scala/test/dev/rudiments/git/GitObjectTest.scala index 8699d55c..d3455e2b 100644 --- a/git/src/test/scala/test/dev/rudiments/git/GitObjectTest.scala +++ b/git/src/test/scala/test/dev/rudiments/git/GitObjectTest.scala @@ -59,7 +59,7 @@ class GitObjectTest extends AnyWordSpec with Matchers with Log { 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.get.toString) + second <- Reader.read(dir, first.asInstanceOf[Commit].parent.head.toString) } yield (first, tree, second) result match { From 38aa1eba26b0dabf05b055b45b02087964b5ca61 Mon Sep 17 00:00:00 2001 From: gennady Date: Sun, 30 Apr 2023 01:58:13 +0600 Subject: [PATCH 34/75] fix blob with non-string content --- .../main/scala/dev/rudiments/git/GitObject.scala | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/git/src/main/scala/dev/rudiments/git/GitObject.scala b/git/src/main/scala/dev/rudiments/git/GitObject.scala index c8537d16..6dfe630c 100644 --- a/git/src/main/scala/dev/rudiments/git/GitObject.scala +++ b/git/src/main/scala/dev/rudiments/git/GitObject.scala @@ -10,6 +10,7 @@ import java.nio.file.{Files, Path} import java.time.{Instant, LocalDateTime, ZoneId, ZonedDateTime} import java.time.format.{DateTimeFormatter, DateTimeFormatterBuilder, SignStyle} import java.time.temporal.ChronoField +import scala.collection.immutable.ArraySeq import scala.collection.mutable import scala.util.matching.Regex @@ -32,11 +33,13 @@ sealed trait GitObject(kind: String) { } } -final case class Blob(content: String) extends GitObject("blob") { - override def data: Array[Byte] = content.getBytes(UTF_8) +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(new String(data, UTF_8)) + 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") { @@ -89,7 +92,7 @@ final case class Commit( committer: Commit.AuthRecord, message: String, signature: Option[String] = None, - originalData: Option[String] = None + originalData: Option[Seq[Byte]] = None ) extends GitObject("commit") { override def data: Array[Byte] = { originalData match @@ -106,8 +109,7 @@ final case class Commit( } buff.append(s"\n\n$message") buff.toString().getBytes(UTF_8) - case Some(msg) => - msg.getBytes(UTF_8) + case Some(msg) => msg.toArray[Byte] } } object Commit { @@ -133,7 +135,7 @@ object Commit { if(new String(candidate.data, UTF_8) == str) { candidate } else { - candidate.copy(originalData = Some(str)) + candidate.copy(originalData = Some(data.toSeq)) } } From 05a429720ba3a23c3572c0c3b33c6bc446a15fda Mon Sep 17 00:00:00 2001 From: gennady Date: Sun, 30 Apr 2023 02:25:40 +0600 Subject: [PATCH 35/75] tags implemented --- .../scala/dev/rudiments/git/GitObject.scala | 70 +++++++++++++++++-- .../scala/dev/rudiments/git/Repository.scala | 31 ++++++-- 2 files changed, 90 insertions(+), 11 deletions(-) diff --git a/git/src/main/scala/dev/rudiments/git/GitObject.scala b/git/src/main/scala/dev/rudiments/git/GitObject.scala index 6dfe630c..9ead4d69 100644 --- a/git/src/main/scala/dev/rudiments/git/GitObject.scala +++ b/git/src/main/scala/dev/rudiments/git/GitObject.scala @@ -109,6 +109,7 @@ final case class Commit( } buff.append(s"\n\n$message") buff.toString().getBytes(UTF_8) + case Some(msg) => msg.toArray[Byte] } } @@ -124,12 +125,14 @@ object Commit { 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(Parent).map(_.group(1)).toSeq.map(SHA1.fromHex), - asMap(Author).map(AuthRecord.apply).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") + asMap(Field.Message).mkString("\n"), + if(signature.nonEmpty) Some(signature.mkString("\n")) else None ) if(new String(candidate.data, UTF_8) == str) { @@ -158,4 +161,63 @@ object Commit { .atZone(ZoneId.of(reg.group(4))) ) } -} \ No newline at end of file +} + +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)) + } + } +} diff --git a/git/src/main/scala/dev/rudiments/git/Repository.scala b/git/src/main/scala/dev/rudiments/git/Repository.scala index 76a947db..c15a92f1 100644 --- a/git/src/main/scala/dev/rudiments/git/Repository.scala +++ b/git/src/main/scala/dev/rudiments/git/Repository.scala @@ -40,27 +40,44 @@ class Repository(root: Path) extends Log { val initialObjects = objects.size val initialErros = errors.size packEntries.foreach { - case (key, v@Entry(PackObj.Tree, _, data, _, _)) => + case (key, v@Entry(PackObj.Tree, size, data, _, _)) => try { - objects.put(key, Tree(ZLib.unpack(data))) + 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, _, data, _, _)) => + case (key, v@Entry(PackObj.Commit, size, data, _, _)) => try { - objects.put(key, Commit(ZLib.unpack(data))) + 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, _, data, _, _)) => + case (key, v@Entry(PackObj.Blob, size, data, _, _)) => try { - objects.put(key, Blob(ZLib.unpack(data))) + 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, _, _, _, _)) => errors.put(key, (v, "Tag parse are not implemented")) + 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, entry) => errors.put(key, (entry, "Parse are not implemented")) } From 0db6acd9ff790f57908e52154d78f12be2df7048 Mon Sep 17 00:00:00 2001 From: gennady Date: Sun, 30 Apr 2023 04:33:58 +0600 Subject: [PATCH 36/75] data parsing of RefDelta implemented --- .../scala/dev/rudiments/git/GitObject.scala | 23 +++++++++++++++ .../scala/dev/rudiments/git/Repository.scala | 28 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/git/src/main/scala/dev/rudiments/git/GitObject.scala b/git/src/main/scala/dev/rudiments/git/GitObject.scala index 9ead4d69..5c2d36a7 100644 --- a/git/src/main/scala/dev/rudiments/git/GitObject.scala +++ b/git/src/main/scala/dev/rudiments/git/GitObject.scala @@ -5,6 +5,7 @@ import dev.rudiments.utils.{Hashed, SHA1, ZLib} import java.lang import java.lang.{IllegalStateException, StringBuffer} +import java.nio.ByteBuffer import java.nio.charset.StandardCharsets.UTF_8 import java.nio.file.{Files, Path} import java.time.{Instant, LocalDateTime, ZoneId, ZonedDateTime} @@ -221,3 +222,25 @@ object Tag { } } } + +case class RefDelta( + link: SHA1, + deflated: Boolean, + delta: Seq[Byte], + crc: Int +) extends GitObject("ref-delta") { + override def data: Array[Byte] = link.asArray ++ delta +} +object RefDelta { + def apply(data: Array[Byte]): RefDelta = { + val buff = ByteBuffer.wrap(data) + val isDeflated = data.slice(20, 22).toSeq == Seq(120.toByte, -100.toByte) + val slice = data.slice(20, data.length) + new RefDelta( + new SHA1(data.take(20)), + isDeflated, + if(isDeflated) ZLib.unpack(slice).toSeq else slice.toSeq, + buff.getInt(data.length - 4) + ) + } +} diff --git a/git/src/main/scala/dev/rudiments/git/Repository.scala b/git/src/main/scala/dev/rudiments/git/Repository.scala index c15a92f1..7938c255 100644 --- a/git/src/main/scala/dev/rudiments/git/Repository.scala +++ b/git/src/main/scala/dev/rudiments/git/Repository.scala @@ -78,9 +78,37 @@ class Repository(root: Path) extends Log { } 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) From f662be036d18e5e10db49bfc807566f7ac50927e Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 4 May 2023 02:43:13 +0600 Subject: [PATCH 37/75] draft size and offset for Ref Delta --- .../scala/dev/rudiments/git/GitObject.scala | 57 +++++++++++++++++-- .../scala/dev/rudiments/git/Repository.scala | 2 + .../test/dev/rudiments/git/PackTest.scala | 41 ++++++++++++- 3 files changed, 92 insertions(+), 8 deletions(-) diff --git a/git/src/main/scala/dev/rudiments/git/GitObject.scala b/git/src/main/scala/dev/rudiments/git/GitObject.scala index 5c2d36a7..59b9b707 100644 --- a/git/src/main/scala/dev/rudiments/git/GitObject.scala +++ b/git/src/main/scala/dev/rudiments/git/GitObject.scala @@ -225,22 +225,67 @@ object Tag { case class RefDelta( link: SHA1, + deltas: Seq[Deltified], deflated: Boolean, - delta: Seq[Byte], - crc: Int + original: Seq[Byte] //TODO -> Seq[Delta], ) extends GitObject("ref-delta") { - override def data: Array[Byte] = link.asArray ++ delta + override def data: Array[Byte] = link.asArray ++ ZLib.pack(original.toArray[Byte]) } object RefDelta { def apply(data: Array[Byte]): RefDelta = { - val buff = ByteBuffer.wrap(data) 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)), + Seq.empty,//Deltified.fromBytes(unpacked), isDeflated, - if(isDeflated) ZLib.unpack(slice).toSeq else slice.toSeq, - buff.getInt(data.length - 4) + 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) { + val t = buff.get() + if (t == 0x80.toByte) { + deltas += Deltified.Copy( + variableSize(buff), + variableSize(buff) + ) + } else if (t == 0x01.toByte) { + deltas += Deltified.Add(0, Seq.empty) + } else if (t == 0x60.toByte) { + assume(!buff.hasRemaining, "Met 0x60, expecting it is the end of the Delta") + } else { + throw new IllegalArgumentException("Doesn't look like deltified instruction") + } + } + deltas.toSeq + } + + 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 + } + + private def nextIsSize(b: Byte): Boolean = (b & 0x80).toByte == -128.toByte +} diff --git a/git/src/main/scala/dev/rudiments/git/Repository.scala b/git/src/main/scala/dev/rudiments/git/Repository.scala index 7938c255..e592e046 100644 --- a/git/src/main/scala/dev/rudiments/git/Repository.scala +++ b/git/src/main/scala/dev/rudiments/git/Repository.scala @@ -17,6 +17,8 @@ class Repository(root: Path) extends Log { 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")) diff --git a/git/src/test/scala/test/dev/rudiments/git/PackTest.scala b/git/src/test/scala/test/dev/rudiments/git/PackTest.scala index c2007242..90058e10 100644 --- a/git/src/test/scala/test/dev/rudiments/git/PackTest.scala +++ b/git/src/test/scala/test/dev/rudiments/git/PackTest.scala @@ -1,13 +1,14 @@ package test.dev.rudiments.git -import dev.rudiments.git.Pack +import dev.rudiments.git.{Deltified, Pack, RefDelta} import dev.rudiments.git.Pack.PackObj -import dev.rudiments.utils.Log +import dev.rudiments.utils.{Hashed, Log} import org.junit.runner.RunWith import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec import org.scalatestplus.junit.JUnitRunner +import java.nio.ByteBuffer import java.nio.file.{Files, Path} @RunWith(classOf[JUnitRunner]) @@ -19,4 +20,40 @@ class PackTest extends AnyWordSpec with Matchers with Log { val readen = Pack.readPack(dir, hash) readen.objects.size should be (367) } + + "can parse ref delta from byte array" in { + 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) + Deltified.variableSize(buff) should be (197) // offset? + buff.position() should be (3) + Deltified.variableSize(buff) should be (24584) // result size! + buff.position() should be (6) + Deltified.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 +*/ } From 4079633f2e42a8b0a435e3f6d69bb219d0a09c7e Mon Sep 17 00:00:00 2001 From: gennady Date: Sat, 6 May 2023 16:01:05 +0600 Subject: [PATCH 38/75] make some files into example module --- .../main/scala/dev/rudiments/app/Main.scala | 7 +++ .../test/dev/rudiments/app}/CheckTest.scala | 2 +- git/src/main/sw.sc | 47 ------------------- 3 files changed, 8 insertions(+), 48 deletions(-) create mode 100644 example/src/main/scala/dev/rudiments/app/Main.scala rename {core/src/test/scala/test/dev/rudiments => example/src/test/scala/test/dev/rudiments/app}/CheckTest.scala (91%) delete mode 100644 git/src/main/sw.sc diff --git a/example/src/main/scala/dev/rudiments/app/Main.scala b/example/src/main/scala/dev/rudiments/app/Main.scala new file mode 100644 index 00000000..162f68c7 --- /dev/null +++ b/example/src/main/scala/dev/rudiments/app/Main.scala @@ -0,0 +1,7 @@ +package dev.rudiments.app + +import dev.rudiments.utils.Log + +object Main extends App with Log { + log.info("Main app") +} diff --git a/core/src/test/scala/test/dev/rudiments/CheckTest.scala b/example/src/test/scala/test/dev/rudiments/app/CheckTest.scala similarity index 91% rename from core/src/test/scala/test/dev/rudiments/CheckTest.scala rename to example/src/test/scala/test/dev/rudiments/app/CheckTest.scala index c2f41b46..68ad425a 100644 --- a/core/src/test/scala/test/dev/rudiments/CheckTest.scala +++ b/example/src/test/scala/test/dev/rudiments/app/CheckTest.scala @@ -1,4 +1,4 @@ -package test.dev.rudiments +package test.dev.rudiments.app import org.junit.runner.RunWith import org.scalatest.matchers.should.Matchers diff --git a/git/src/main/sw.sc b/git/src/main/sw.sc deleted file mode 100644 index 0c081063..00000000 --- a/git/src/main/sw.sc +++ /dev/null @@ -1,47 +0,0 @@ -import java.nio.charset.StandardCharsets.UTF_8 -import dev.rudiments.git.Commit - -val hash = "81e30fc0c74ec502f99fd2f91784bfe1606e6a9f" - -val str = """tree af1dd2b5ea96f90a06bd8b965ff784898782d7d2 - |parent 55095d03a9272ad936d8cf6e23a46e4fcc1da98d - |author Yi-Jyun Pan 1628920089 +0800 - |committer Yi-Jyun Pan 1628920919 +0800 - |gpgsig -----BEGIN PGP SIGNATURE----- - | - | iQIzBAABCAAdFiEEs0GfOWTlygReNS0yQhVLGxz+M3cFAmEXXFkACgkQQhVLGxz+ - | M3d2Cw//R5H79nED3KRVmOoDNfpncopOf8m7dTc8Z+BIigtIVhQeIwnPih52u6tP - | susk9ZyjaDXFJ6bqUBXKuXcFmttiw1Yklr15rm79HFceZyzCFZpw/HqNm/Wnx8sE - | GYEoZ35tjuBVMog8zuRfXQNtfqwCLvUPk5xU0J1+OCM1TMkS5iLFOuvO0/x4kUNL - | ARRi5bIvZgrS4+BqXuTMk2L64Ev+R8WdAOvUFsgwgwlH++UnHUJkIEaf9kczLgr8 - | Dg0lcZGdMlTKDEgLQJ0BDrFZX0eoXA8/yi3sBvqQ/lIBszznxZViNcFMierVvPAz - | g1jWmyvo2s6MMmA/lVvS4/D1XyQPf5p09BhVBogqKk9U+2Rqun35QjtH2Wtb22AJ - | xBsFt3uZ3MgenTc9TF8yXe4DZK60XHqXDIrslAUvc9LQ2QL4dXE7FsSaQVi49mls - | 3nzfK5tEMU2cEjX7DYxhfSWsh1g7E+GA2fYelol1bhbPOmUcVk5FyGj42YVn7o3d - | xsdwWWYAePNuToeuoMXCdkIioQHoHx1MC4k6IzEK0lFJQ/998C1MPM/K8uMOkWdZ - | ag48tK54+0Nvek6ULAbWS9IOTyviaHtEoaUtVIfPDDhMhGekXW5OFEFV3eJwPJ/H - | XMwETjmmWoUFoOiLomtCxR2cihdjrE9OPsgseXMIJ1oLGxPc9sE= - | =14DB - | -----END PGP SIGNATURE----- - | - |l10n: zh_TW.po: update for v2.33.0 rnd 2 - | - |Signed-off-by: Yi-Jyun Pan - |""".stripMargin - - - -val treeR = """tree (?\w{40})""".r -val parentR = """parent (?\w{40})""".r -val authorR = """author (?.+) <(?.+)> (?\d{10,20}) (?.+)""".r -val committerR = """committer (?.+) <(?.+)> (?\d{10,20}) (?.+)""".r -val signatureR = """gpgsig -----BEGIN PGP SIGNATURE-----(.*\n)* -----END PGP SIGNATURE-----\n""".r - -val tree = treeR.findAllMatchIn(str).map(_.group(1)).toSeq.head -val parent = parentR.findAllMatchIn(str).map(_.group(1)).toSeq -val author = authorR.findAllMatchIn(str).map(_.group(1)).toSeq.head -val committer = committerR.findAllMatchIn(str).map(_.group(1)).toSeq.head - -val signature = signatureR.findAllMatchIn(str).mkString("|") - -val commit = Commit.apply(str.getBytes(UTF_8)) \ No newline at end of file From 486fc4932930ea31dbd5aaf45b2760fad242248b Mon Sep 17 00:00:00 2001 From: gennady Date: Sat, 6 May 2023 16:04:32 +0600 Subject: [PATCH 39/75] ignore pack test, based on arbitrary id --- git/src/test/scala/test/dev/rudiments/git/PackTest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/src/test/scala/test/dev/rudiments/git/PackTest.scala b/git/src/test/scala/test/dev/rudiments/git/PackTest.scala index 90058e10..4753600f 100644 --- a/git/src/test/scala/test/dev/rudiments/git/PackTest.scala +++ b/git/src/test/scala/test/dev/rudiments/git/PackTest.scala @@ -15,7 +15,7 @@ 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" in { + "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) From e9f3c1b713e8680200db1fe93d7acfb97cb7b62d Mon Sep 17 00:00:00 2001 From: gennady Date: Sun, 7 May 2023 13:19:44 +0600 Subject: [PATCH 40/75] restore codecs module, toss some things around --- codecs/build.gradle | 3 +++ .../scala/dev/rudiments/codecs}/BytesCodec.scala | 2 +- codecs/src/test/resources/application.conf | 14 ++++++++++++++ codecs/src/test/resources/logback.xml | 14 ++++++++++++++ .../test/dev/rudiments/codecs/CheckTest.scala | 14 ++++++++++++++ file/build.gradle | 1 + .../main/scala/dev/rudiments/file/Repository.scala | 2 +- .../main/scala/dev/rudiments/git/GitObject.scala | 4 ++-- settings.gradle | 1 + 9 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 codecs/build.gradle rename {core/src/main/scala/dev/rudiments/utils => codecs/src/main/scala/dev/rudiments/codecs}/BytesCodec.scala (96%) create mode 100644 codecs/src/test/resources/application.conf create mode 100644 codecs/src/test/resources/logback.xml create mode 100644 codecs/src/test/scala/test/dev/rudiments/codecs/CheckTest.scala diff --git a/codecs/build.gradle b/codecs/build.gradle new file mode 100644 index 00000000..378848af --- /dev/null +++ b/codecs/build.gradle @@ -0,0 +1,3 @@ +dependencies { + implementation project(':core') +} \ No newline at end of file diff --git a/core/src/main/scala/dev/rudiments/utils/BytesCodec.scala b/codecs/src/main/scala/dev/rudiments/codecs/BytesCodec.scala similarity index 96% rename from core/src/main/scala/dev/rudiments/utils/BytesCodec.scala rename to codecs/src/main/scala/dev/rudiments/codecs/BytesCodec.scala index 03af1899..ad2fd046 100644 --- a/core/src/main/scala/dev/rudiments/utils/BytesCodec.scala +++ b/codecs/src/main/scala/dev/rudiments/codecs/BytesCodec.scala @@ -1,4 +1,4 @@ -package dev.rudiments.utils +package dev.rudiments.codecs import java.nio.ByteBuffer import java.nio.charset.StandardCharsets.UTF_8 diff --git a/codecs/src/test/resources/application.conf b/codecs/src/test/resources/application.conf new file mode 100644 index 00000000..cd6a8ae3 --- /dev/null +++ b/codecs/src/test/resources/application.conf @@ -0,0 +1,14 @@ +http { + 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/codecs/src/test/resources/logback.xml b/codecs/src/test/resources/logback.xml new file mode 100644 index 00000000..a994bdfc --- /dev/null +++ b/codecs/src/test/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + %d{HH:mm:ss.SSS} %-5level %-20logger{20} %msg%n + + + + + + + + + \ No newline at end of file diff --git a/codecs/src/test/scala/test/dev/rudiments/codecs/CheckTest.scala b/codecs/src/test/scala/test/dev/rudiments/codecs/CheckTest.scala new file mode 100644 index 00000000..56ec05e1 --- /dev/null +++ b/codecs/src/test/scala/test/dev/rudiments/codecs/CheckTest.scala @@ -0,0 +1,14 @@ +package test.dev.rudiments.codecs + +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 CheckTest extends AnyWordSpec with Matchers { + "always true" in { + val a = true + a should be(true) + } +} diff --git a/file/build.gradle b/file/build.gradle index e4dbb7fe..09d68f1a 100644 --- a/file/build.gradle +++ b/file/build.gradle @@ -1,3 +1,4 @@ dependencies { implementation project(':core') + implementation project(':codecs') } diff --git a/file/src/main/scala/dev/rudiments/file/Repository.scala b/file/src/main/scala/dev/rudiments/file/Repository.scala index 481837d3..04523bc5 100644 --- a/file/src/main/scala/dev/rudiments/file/Repository.scala +++ b/file/src/main/scala/dev/rudiments/file/Repository.scala @@ -1,6 +1,6 @@ package dev.rudiments.file -import dev.rudiments.utils.BytesCodec +import dev.rudiments.codecs.BytesCodec import dev.rudiments.utils.SHA3 import java.io.File diff --git a/git/src/main/scala/dev/rudiments/git/GitObject.scala b/git/src/main/scala/dev/rudiments/git/GitObject.scala index 59b9b707..b4b4882c 100644 --- a/git/src/main/scala/dev/rudiments/git/GitObject.scala +++ b/git/src/main/scala/dev/rudiments/git/GitObject.scala @@ -56,7 +56,7 @@ object Tree { 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)))) + 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 @@ -237,7 +237,7 @@ object RefDelta { val slice = data.slice(20, data.length) val unpacked = if(isDeflated) ZLib.unpack(slice) else slice new RefDelta( - new SHA1(data.take(20)), + new SHA1(data.take(20).toSeq), Seq.empty,//Deltified.fromBytes(unpacked), isDeflated, unpacked.toSeq diff --git a/settings.gradle b/settings.gradle index 0c00cb08..bd6aa5b7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,6 @@ rootProject.name = 'hardcore' include 'core' +include 'codecs' include 'file' include 'git' include 'example' From 555370bf150a7da0e9dee93ac37e2e2928f3cdd6 Mon Sep 17 00:00:00 2001 From: gennady Date: Sun, 7 May 2023 15:23:06 +0600 Subject: [PATCH 41/75] use milestone circe --- codecs/build.gradle | 3 ++ .../test/dev/rudiments/codecs/CheckTest.scala | 14 -------- .../test/dev/rudiments/codecs/CirceTest.scala | 35 +++++++++++++++++++ .../main/scala/dev/rudiments/utils/ZLib.scala | 10 +++--- 4 files changed, 43 insertions(+), 19 deletions(-) delete mode 100644 codecs/src/test/scala/test/dev/rudiments/codecs/CheckTest.scala create mode 100644 codecs/src/test/scala/test/dev/rudiments/codecs/CirceTest.scala diff --git a/codecs/build.gradle b/codecs/build.gradle index 378848af..b543e0f7 100644 --- a/codecs/build.gradle +++ b/codecs/build.gradle @@ -1,3 +1,6 @@ dependencies { implementation project(':core') + + 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/codecs/src/test/scala/test/dev/rudiments/codecs/CheckTest.scala b/codecs/src/test/scala/test/dev/rudiments/codecs/CheckTest.scala deleted file mode 100644 index 56ec05e1..00000000 --- a/codecs/src/test/scala/test/dev/rudiments/codecs/CheckTest.scala +++ /dev/null @@ -1,14 +0,0 @@ -package test.dev.rudiments.codecs - -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 CheckTest extends AnyWordSpec with Matchers { - "always true" in { - val a = true - a should be(true) - } -} diff --git a/codecs/src/test/scala/test/dev/rudiments/codecs/CirceTest.scala b/codecs/src/test/scala/test/dev/rudiments/codecs/CirceTest.scala new file mode 100644 index 00000000..9a2fd7b2 --- /dev/null +++ b/codecs/src/test/scala/test/dev/rudiments/codecs/CirceTest.scala @@ -0,0 +1,35 @@ +package test.dev.rudiments.codecs + +import io.circe.{Codec, Json} +import io.circe.generic.semiauto.* +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 CirceTest extends AnyWordSpec with Matchers { + case class Sample( + a: Int, + b: String, + c: Seq[String] + ) + + 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/main/scala/dev/rudiments/utils/ZLib.scala b/core/src/main/scala/dev/rudiments/utils/ZLib.scala index 4aad4bd3..5915476a 100644 --- a/core/src/main/scala/dev/rudiments/utils/ZLib.scala +++ b/core/src/main/scala/dev/rudiments/utils/ZLib.scala @@ -4,9 +4,9 @@ import java.io.ByteArrayOutputStream import java.util.zip.{Deflater, Inflater} object ZLib { - val BUFFER_SIZE = 4096 + val DEFAULT_BUFFER_SIZE = 4096 - def pack(data: Array[Byte]): Array[Byte] = { + def pack(data: Array[Byte], size: Int = DEFAULT_BUFFER_SIZE): Array[Byte] = { val deflater = new Deflater() deflater.setLevel(Deflater.DEFAULT_COMPRESSION) deflater.setInput(data) @@ -14,7 +14,7 @@ object ZLib { val outputStream = new ByteArrayOutputStream(data.length) try { deflater.finish() - val buffer = new Array[Byte](BUFFER_SIZE) + val buffer = new Array[Byte](size) while (!deflater.finished) { val count = deflater.deflate(buffer) outputStream.write(buffer, 0, count) @@ -24,13 +24,13 @@ object ZLib { if (outputStream != null) outputStream.close() } - def unpack(data: Array[Byte]): Array[Byte] = { + 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](BUFFER_SIZE) + val buffer = new Array[Byte](size) while (!inflater.finished) { val count = inflater.inflate(buffer) outputStream.write(buffer, 0, count) From 215d42a6f91036363df801b985f2fd67f5605e99 Mon Sep 17 00:00:00 2001 From: gennady Date: Tue, 9 May 2023 20:10:33 +0600 Subject: [PATCH 42/75] some refactoring --- .../scala/dev/rudiments/file/Repository.scala | 32 +++++++++---------- .../main/scala/dev/rudiments/file/Tx.scala | 16 +++++----- .../test/dev/rudiments/file/FileTest.scala | 15 ++++----- 3 files changed, 31 insertions(+), 32 deletions(-) diff --git a/file/src/main/scala/dev/rudiments/file/Repository.scala b/file/src/main/scala/dev/rudiments/file/Repository.scala index 04523bc5..e3697b54 100644 --- a/file/src/main/scala/dev/rudiments/file/Repository.scala +++ b/file/src/main/scala/dev/rudiments/file/Repository.scala @@ -6,22 +6,22 @@ import dev.rudiments.utils.SHA3 import java.io.File import java.lang import java.nio.ByteBuffer -import java.nio.file.Path +import java.nio.file.{Path => FilePath} import scala.collection.immutable.ArraySeq import scala.collection.mutable -class Repository(path: Path) { +class Repository(path: FilePath) { private val dir = path.toAbsolutePath - val state: mutable.Map[Rel, FileData] = mutable.Map.empty + val state: mutable.Map[Path, FS] = mutable.Map.empty val log: mutable.Buffer[Commit] = mutable.Buffer.empty - val ignored: Set[Rel] = Set(Seq(".git"), Seq(".gradle"), Seq(".idea")) + val ignored: Set[Path] = Set(Seq(".git"), Seq(".gradle"), Seq(".idea")) def read(): Unit = { given tx: Tx = new Tx(state.toMap) val file = dir.toFile - val readen: FileData = if(file.isFile) { + val readen: FS = if(file.isFile) { readFileUnsafe(file) } else if(file.isDirectory) { readDirUnsafe(file, Seq.empty) @@ -37,7 +37,7 @@ class Repository(path: Path) { } } - private def readDirUnsafe(file: File, prefix: Rel)(using tx: Tx): Dir = { + private def readDirUnsafe(file: File, prefix: Path)(using tx: Tx): Dir = { val content = file.listFiles().toList.sortBy(_.getName) .filterNot(f => ignored.contains(prefix :+ f.getName)) .map { @@ -49,35 +49,35 @@ class Repository(path: Path) { val (dirs, blobs) = content.partitionMap { case (s, _: Dir) => Left(s) - case (s, _: Blob) => Right(s) + case (s, _: Binary) => Right(s) } Dir(dirs, blobs) } - private def readFileUnsafe(file: File): Blob = { - Blob(java.nio.file.Files.readAllBytes(file.toPath)) + private def readFileUnsafe(file: File): Binary = { + Binary(java.nio.file.Files.readAllBytes(file.toPath)) } } -type Rel = Seq[String] +type Path = Seq[String] -sealed trait FileData { +sealed trait FS { def about: About } import dev.rudiments.file.About.Type -case class Dir(directions: Rel, files: Rel) extends FileData { +case class Dir(directions: Path, files: Path) extends FS { lazy val data: Array[Byte] = BytesCodec.encodeStrings(directions) ++ BytesCodec.encodeStrings(files) override def about: About = About(Type.Dir, data.length, SHA3(data)) } -case class Blob(data: Seq[Byte]) extends FileData { +case class Binary(data: Seq[Byte]) extends FS { override def about: About = About(Type.File, data.size, SHA3(data.toArray[Byte])) } -object Blob { - def apply(data: Array[Byte]): Blob = new Blob(ArraySeq.unsafeWrapArray(data)) +object Binary { + def apply(data: Array[Byte]): Binary = new Binary(ArraySeq.unsafeWrapArray(data)) } -case object NotExist extends FileData { +case object NotExist extends FS { override def about: About = About(Type.File, 0, SHA3.empty) } \ No newline at end of file diff --git a/file/src/main/scala/dev/rudiments/file/Tx.scala b/file/src/main/scala/dev/rudiments/file/Tx.scala index 00c501cc..569a54d5 100644 --- a/file/src/main/scala/dev/rudiments/file/Tx.scala +++ b/file/src/main/scala/dev/rudiments/file/Tx.scala @@ -4,11 +4,11 @@ import dev.rudiments.utils.SHA3 import scala.collection.mutable -class Tx(val initial: Map[Rel, FileData]) { - val changed: mutable.Map[Rel, FileData] = mutable.Map.empty - val log: mutable.Map[Rel, FileLog] = mutable.Map.empty +class Tx(val initial: Map[Path, FS]) { + val changed: mutable.Map[Path, FS] = mutable.Map.empty + val log: mutable.Map[Path, FileLog] = mutable.Map.empty - def put(k: Rel, v: FileData): Unit = { + def put(k: Path, v: FS): Unit = { initial.get(k) match case Some(found) => if (found.about != v.about) { @@ -41,14 +41,14 @@ class Tx(val initial: Map[Rel, FileData]) { def makeCommit: Commit = Commit(changed.toMap.map((k,change) => k -> (log(k), change))) - def deleting(from: Rel): Unit = { //TODO replace with Delete on Dir instead of Repo + def deleting(from: Path): Unit = { //TODO replace with Delete on Dir instead of Repo initial.get(from) match { case Some(d@Dir(files, dirs)) => changed.put(from, NotExist) log.put(from, FileLog(None, Some(d.about.checksum))) dirs.foreach(s => deleting(from :+ s)) files.foreach(s => deleting(from :+ s)) - case Some(b: Blob) => + case Some(b: Binary) => changed.put(from, NotExist) log.put(from, FileLog(None, Some(b.about.checksum))) case _ => //DO nothing @@ -63,7 +63,7 @@ case class FileLog( ) case class Commit( - changes: Map[Rel, (FileLog, FileData)] + changes: Map[Path, (FileLog, FS)] ) { - def changed: Map[Rel, FileData] = changes.map { case (k, (_, change)) => k -> change } + def changed: Map[Path, FS] = changes.map { case (k, (_, change)) => k -> change } } \ No newline at end of file diff --git a/file/src/test/scala/test/dev/rudiments/file/FileTest.scala b/file/src/test/scala/test/dev/rudiments/file/FileTest.scala index 884ffd92..efa3f9bb 100644 --- a/file/src/test/scala/test/dev/rudiments/file/FileTest.scala +++ b/file/src/test/scala/test/dev/rudiments/file/FileTest.scala @@ -8,35 +8,34 @@ import org.scalatest.wordspec.AnyWordSpec import org.scalatestplus.junit.JUnitRunner import java.nio.charset.StandardCharsets.UTF_8 -import java.nio.file.Path - +import java.nio.file.{Path => FilePath} @RunWith(classOf[JUnitRunner]) class FileTest extends AnyWordSpec with Matchers { - private val dir = Path.of("..", "file", "src", "test", "resources", "example").toAbsolutePath + private val dir = FilePath.of("..", "file", "src", "test", "resources", "example").toAbsolutePath val repo = new Repository(dir) "can read repository" in { repo.read() repo.state.size should be (4) repo.state.toMap should be ( - Map[Rel, FileData]( + Map[Path, FS]( Seq.empty -> Dir(Seq("nested"), Seq("1.txt")), Seq("nested") -> Dir(Seq.empty, Seq("2.txt")), - Seq("nested", "2.txt") -> Blob("second file".getBytes(UTF_8)), - Seq("1.txt") -> Blob("first file".getBytes(UTF_8)) + Seq("nested", "2.txt") -> Binary("second file".getBytes(UTF_8)), + Seq("1.txt") -> Binary("first file".getBytes(UTF_8)) ) ) } "can read hole project" ignore { - val r = new Repository(Path.of(".")) + val r = new Repository(FilePath.of(".")) r.read() r.log.size should be (1) } "can read git project" ignore { - val r = new Repository(Path.of("../git")) + val r = new Repository(FilePath.of("../git")) r.read() r.log.size should be(1) } From ccc6f2c0a24378f9d1e9f745d125c671b3103f41 Mon Sep 17 00:00:00 2001 From: gennady Date: Mon, 22 May 2023 23:14:51 +0600 Subject: [PATCH 43/75] draft git pack delta decoder (NOT WORKING) --- example/build.gradle | 3 + .../scala/dev/rudiments/git/ByteUtils.scala | 77 +++++++++++++++++++ .../scala/dev/rudiments/git/GitObject.scala | 33 +------- .../main/scala/dev/rudiments/git/Pack.scala | 2 +- .../test/dev/rudiments/git/PackTest.scala | 10 +-- 5 files changed, 88 insertions(+), 37 deletions(-) create mode 100644 git/src/main/scala/dev/rudiments/git/ByteUtils.scala diff --git a/example/build.gradle b/example/build.gradle index d9e2ff40..7a9244a4 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -1,5 +1,8 @@ dependencies { implementation project(':core') + implementation project(':codecs') + implementation project(':file') + implementation project(':git') implementation 'ch.qos.logback:logback-classic:1.4.5' } 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..3f1464dd --- /dev/null +++ b/git/src/main/scala/dev/rudiments/git/ByteUtils.scala @@ -0,0 +1,77 @@ +package dev.rudiments.git + +import dev.rudiments.git.Pack.PackObj + +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 index b4b4882c..d707b057 100644 --- a/git/src/main/scala/dev/rudiments/git/GitObject.scala +++ b/git/src/main/scala/dev/rudiments/git/GitObject.scala @@ -238,7 +238,7 @@ object RefDelta { val unpacked = if(isDeflated) ZLib.unpack(slice) else slice new RefDelta( new SHA1(data.take(20).toSeq), - Seq.empty,//Deltified.fromBytes(unpacked), + Deltified.fromBytes(unpacked), isDeflated, unpacked.toSeq ) @@ -255,37 +255,8 @@ object Deltified { val buff = ByteBuffer.wrap(data) val deltas = mutable.Buffer.empty[Deltified] while (buff.hasRemaining) { - val t = buff.get() - if (t == 0x80.toByte) { - deltas += Deltified.Copy( - variableSize(buff), - variableSize(buff) - ) - } else if (t == 0x01.toByte) { - deltas += Deltified.Add(0, Seq.empty) - } else if (t == 0x60.toByte) { - assume(!buff.hasRemaining, "Met 0x60, expecting it is the end of the Delta") - } else { - throw new IllegalArgumentException("Doesn't look like deltified instruction") - } + deltas += ByteUtils.delta(buff) } deltas.toSeq } - - 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 - } - - private def nextIsSize(b: Byte): Boolean = (b & 0x80).toByte == -128.toByte } diff --git a/git/src/main/scala/dev/rudiments/git/Pack.scala b/git/src/main/scala/dev/rudiments/git/Pack.scala index e6720345..2364c4fb 100644 --- a/git/src/main/scala/dev/rudiments/git/Pack.scala +++ b/git/src/main/scala/dev/rudiments/git/Pack.scala @@ -85,7 +85,7 @@ object Pack { buf.put(0, bytes.slice(8, 12)).getInt } - private def readEntry(bytes: Array[Byte], from: Int, until: Int): Entry = { + 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) diff --git a/git/src/test/scala/test/dev/rudiments/git/PackTest.scala b/git/src/test/scala/test/dev/rudiments/git/PackTest.scala index 4753600f..c61b6373 100644 --- a/git/src/test/scala/test/dev/rudiments/git/PackTest.scala +++ b/git/src/test/scala/test/dev/rudiments/git/PackTest.scala @@ -1,6 +1,6 @@ package test.dev.rudiments.git -import dev.rudiments.git.{Deltified, Pack, RefDelta} +import dev.rudiments.git.{ByteUtils, Deltified, Pack, RefDelta} import dev.rudiments.git.Pack.PackObj import dev.rudiments.utils.{Hashed, Log} import org.junit.runner.RunWith @@ -21,7 +21,7 @@ class PackTest extends AnyWordSpec with Matchers with Log { readen.objects.size should be (367) } - "can parse ref delta from byte array" in { + "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) @@ -31,11 +31,11 @@ class PackTest extends AnyWordSpec with Matchers with Log { val buff = ByteBuffer.wrap(delta.original.toArray[Byte]) buff.position() should be (0) buff.get() should be (0x80.toByte) - Deltified.variableSize(buff) should be (197) // offset? + ByteUtils.variableSize(buff) should be (197) // offset? buff.position() should be (3) - Deltified.variableSize(buff) should be (24584) // result size! + ByteUtils.variableSize(buff) should be (24584) // result size! buff.position() should be (6) - Deltified.variableSize(buff) should be (1072) + ByteUtils.variableSize(buff) should be (1072) buff.position() should be (8) buff.get() should be (0x60.toByte) } From 55c5f9c0cd69519ef330f120a4ea01a4418834a2 Mon Sep 17 00:00:00 2001 From: gennady Date: Sun, 4 Jun 2023 13:09:00 +0600 Subject: [PATCH 44/75] draft tables --- .../scala/dev/rudiments/hardcore/CRUD.scala | 43 ++-- .../dev/rudiments/hardcore/Location.scala | 54 ----- .../dev/rudiments/hardcore/Message.scala | 2 +- .../scala/dev/rudiments/hardcore/Node.scala | 184 ------------------ .../scala/dev/rudiments/hardcore/Root.scala | 7 - .../scala/dev/rudiments/hardcore/Table.scala | 101 ++++++++++ .../scala/dev/rudiments/hardcore/Thing.scala | 14 +- .../scala/dev/rudiments/hardcore/Tx.scala | 11 -- .../scala/dev/rudiments/utils/SaltedMap.scala | 2 - .../dev/rudiments/hardcore/NodeSpec.scala | 60 ------ .../dev/rudiments/hardcore/TableSpec.scala | 66 +++++++ 11 files changed, 191 insertions(+), 353 deletions(-) delete mode 100644 core/src/main/scala/dev/rudiments/hardcore/Location.scala delete mode 100644 core/src/main/scala/dev/rudiments/hardcore/Node.scala delete mode 100644 core/src/main/scala/dev/rudiments/hardcore/Root.scala create mode 100644 core/src/main/scala/dev/rudiments/hardcore/Table.scala delete mode 100644 core/src/main/scala/dev/rudiments/hardcore/Tx.scala delete mode 100644 core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala create mode 100644 core/src/test/scala/test/dev/rudiments/hardcore/TableSpec.scala diff --git a/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala b/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala index 9a186490..63c19691 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala @@ -6,37 +6,24 @@ sealed trait CRUD {} object CRUD {} case class Create(value: Product) extends Command with CRUD -case class Read(where: Location) extends Query with CRUD +case class Read[K](where: K) extends Query with CRUD case class Update(old: Product, value: Product) extends Command with CRUD case class Delete(old: Product) extends Command with CRUD +case class Move[K](from: K, to: K) extends Command with CRUD -case class Created(value: Product) extends Event with CRUD -case class Readen(value: Product) extends Report with CRUD -case class Updated(old: Product, value: Product) extends Event with CRUD -case class Deleted(old: Product) extends Event with CRUD -case class Commit(events: (Location, Event with CRUD)*) extends Event with CRUD { - private val indexed = events.toMap - if(indexed.size != events.size) { - throw new IllegalArgumentException("Only unique locations allowed") - } +case class Created[V](value: V) extends Event with CRUD +case class Readen[V](value: V) extends Report with CRUD +case class Updated[V](old: V, value: V) extends Event with CRUD +case class Deleted[V](old: V) extends Event with CRUD +case class Moved[K, V](value: V, to: K) extends Event with CRUD +case class Tx[K, V](events: (K, CUD[K, V])*) extends Event with CRUD - def flatten: Seq[(Location, Event with CRUD)] = events.foldLeft(Seq.empty[(Location, Event with CRUD)]) { - case (m, (prefix, c: Commit)) => m ++ c.flatten.map { case (l, evt) => prefix / l -> evt } - case (m, p) => m :+ p //Created, Updated, Deleted - } //TODO reject commit with intersecting locations inside? - - override def toString: String = events.map { (l, evt) => evt match - case Created(v) => s"$l +$v" - case Updated(v1, v2) => s"$l *$v1|->$v2" - case Deleted(v) => s"$l -$v" - case c: Commit => s"$l: Commit($c)" - case other => s"$l -> {$other}" - }.mkString(";") -} - -case class NotFound(id: Location) extends Report with CRUD +case class NotFound[K](where: K) extends Report with CRUD case object Identical extends Report with CRUD -case class Conflict(incoming: Event, actual: Out) extends Error with CRUD -case class NotSupported(in: In) extends Error with CRUD -case class MultiError(errors: (Location, Out)*) extends Error with CRUD +case class TxReport[K, V](ok: Seq[(K, CUD[K, V])], errors: Seq[(K, Report)]) extends Report with CRUD +case class Conflict(in: Event, actual: Out) extends Error with CRUD +case class NotSupported(m: Message) extends Error with CRUD case class InternalError(t: Throwable) extends Error with CRUD + +type CUD[K, V] = Created[V] | Updated[V] | Deleted[V] | Moved[K, V] +type CUDR[K, V] = CUD[K, V] | Report \ No newline at end of file diff --git a/core/src/main/scala/dev/rudiments/hardcore/Location.scala b/core/src/main/scala/dev/rudiments/hardcore/Location.scala deleted file mode 100644 index 545f452a..00000000 --- a/core/src/main/scala/dev/rudiments/hardcore/Location.scala +++ /dev/null @@ -1,54 +0,0 @@ -package dev.rudiments.hardcore - -import java.lang - -sealed trait Location extends Product { - def /(l: Location): Location - final def toIds: Seq[ID] = this match - case Self => Seq.empty - case id: ID => id :: Nil - case path: Path => path.ids -} -object Location { - def apply(ids: ID*): Location = ids match - case Nil => Self - case h :: Nil => h - case _ => Path(ids:_*) -} - -case object Self extends Location { - def /(l: Location): Location = l match { - case Self => Self - case id: ID => id - case path: Path => path - } -} - -final case class ID(key: Any) extends Location { - def /(l: Location): Location = l match { - case Self => this // or Path(id, Self)? - case id: ID => Path(this, id) - case path: Path => Path(this +: path.ids: _*) - } -} - -final case class Path(ids: ID*) extends Location { - if(ids.size < 2) - throw new IllegalArgumentException(s"Path should be at least with 2 IDs, but got: ${ids.size}") - - def /(l: Location): Location = l match { - case Self => this // or Path(ids :+ Self)? - case id: ID => Path(ids :+ id: _*) - case path: Path => Path(ids ++ path.ids: _*) - } - - def head: ID = ids.head - def tail: Location = - if (ids.size == 2) { - ids.last - } else { - Path(ids.tail :_*) - } - - override def toString: String = ids.map(_.key.toString).mkString("/") -} diff --git a/core/src/main/scala/dev/rudiments/hardcore/Message.scala b/core/src/main/scala/dev/rudiments/hardcore/Message.scala index 8e08d66b..7b2b0cd4 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Message.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Message.scala @@ -8,4 +8,4 @@ trait Command extends In trait Query extends In trait Event extends Out trait Report extends Out -trait Error extends Out \ No newline at end of file +trait Error extends Report \ 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 56cd2c47..00000000 --- a/core/src/main/scala/dev/rudiments/hardcore/Node.scala +++ /dev/null @@ -1,184 +0,0 @@ -package dev.rudiments.hardcore - -import dev.rudiments.hardcore.Node.Cursor - -import java.lang -import scala.annotation.tailrec -import scala.collection.immutable.ListMap -import scala.collection.mutable - -case class Node( - state: mutable.Map[Location, Product] = mutable.Map.empty -) { - private def cursor() = new Node.Cursor(this) - - def read(l: Location): Out with CRUD = cursor().search(l).out - - def apply(l: Location, event: Event with CRUD): Out with CRUD = { - cursor() - .search(l) - .check(event) - .unsafeApply() - } - - def apply(commit: Commit): Out with CRUD = { - val (errors, events) = commit.flatten - .map { (l, evt) => l -> cursor().search(l).check(evt) } - .partitionMap { (l, c) => c.out match - case _: Event with CRUD => Right(l -> c) - case _ => Left(l -> c.out) - } - - if (errors.isEmpty) { - try { - val executed = events - .map { (l, cur) => l -> cur.unsafeApply() } //TODO rollback if errors - .filter { _._2 match - case _: Event with CRUD => false - case _ => true - } - if(executed.isEmpty) commit - else MultiError(executed:_*) - } catch { - case e: Exception => InternalError(e) - } - } else { - MultiError(errors:_*) - } - } - - def compare(from: Location, to: Location): Out with CRUD = { - (read(from), read(to)) match - case (Readen(f: Node), Readen(t: Node)) => f.reconsileTo(t) match - case Nil => Identical - case evts => Commit(evts:_*) - case (_: NotFound, Readen(t)) => Created(t) - case (Readen(f), Readen(t)) if f != t => Updated(f, t) - case (_: Readen, _: Readen) => Identical - case (Readen(f), _: NotFound) => Deleted(f) - case (nf1: NotFound, _: NotFound) => nf1 - case other => throw new IllegalArgumentException(s"should never happen with $other") - } - - def reconsileTo(node: Node): Seq[(Location, Event with CRUD)] = { - if (this == node) { - Seq.empty - } else { - val keys = this.state.keySet ++ node.state.keySet - keys.foldLeft(Seq.empty[(Location, Event with CRUD)]) { (out, key) => - (this.state.get(key), node.state.get(key)) match - case (Some(f: Node), Some(t: Node)) => out ++ f.reconsileTo(t).map { (l, e) => key / l -> e } - case (None, Some(t)) => out :+ key -> Created(t) - case (Some(f), Some(t)) if f != t => out :+ key -> Updated(f, t) - case (Some(f), Some(t)) if f == t => out - case (Some(f), None) => out :+ key -> Deleted(f) - case other => throw new IllegalArgumentException(s"should never happen with $other") - } - } - } - - def size: Int = this.state.size - - def >+ (pair: (Location, Product)): Out with CRUD = this.apply(pair._1, Created(pair._2)) - def >* (pair: (Location, Product)): Out with CRUD = this.read(pair._1) match { - case Readen(r) => this.apply(pair._1, Updated(r, pair._2)) - case other => other - } - def >- (l: Location): Out with CRUD = this.read(l) match { - case Readen(r) => this.apply(l, Deleted(r)) - case other => other - } -} - -object Node { - def empty: Node = new Node() - - def from(c: Commit): Either[MultiError, Node] = { - val node = Node.empty - node.apply(c) match { - case _: Commit => Right(node) - case m: MultiError => Left(m) - case other => - throw new IllegalArgumentException(s"Should never happen: $other") - } - } - - final private class Cursor(starting: Node) { - var node: Node = starting - var to: List[ID] = Nil - var in: List[ID] = Nil - var out: Out with CRUD = _ - - def search(l: Location): Cursor = { - searchIds(l.toIds.toList) - this - } - - @tailrec - private def searchIds(ids: List[ID]): Unit = ids match { - case Nil => node.state.get(Self) match - case Some(v) => out = Readen(v) - case None => out = NotFound(Self) - case id :: Nil => node.state.get(id) match - case Some(n: Node) => - node = n - to = id +: to - out = Readen(n) - case Some(v) => - in = id :: Nil - out = Readen(v) - case None => - in = id :: Nil - out = NotFound(id) - case h :: t => node.state.get(h) match - case Some(n: Node) => - node = n - to = h +: to - searchIds(t) - case Some(_) => - in = h :: Nil - out = NotSupported(Read(Location(t:_*))) - case None => - in = Nil - out = NotFound(Location(ids:_*)) - } - - def inLocation: Location = Location(in:_*) - - def location: Location = Location(to ++ in: _*) - - def check(event: Event with CRUD): Cursor = { - out = (out, event) match - case (NotFound(_), c: Created) => c - case (Readen(v), u@Updated(v1, v2)) if v == v1 && v1 != v2 => u - case (r@Readen(v), Updated(v1, v2)) if v == v1 && v1 == v2 => r - case (Readen(v), d@Deleted(old)) if v == old => d - case (Readen(_), c: Commit) => - val errors = c.flatten - .map { (l, evt) => l -> node.cursor().search(l).check(evt).out } - .filter { _._2 match - case _: Event with CRUD => false - case _ => true - } - - if (errors.isEmpty) { - c - } else { - MultiError(errors: _*) - } - case (nf: NotFound, _) => nf - case (actual, event) => Conflict(event, actual) - - this - } - - def unsafeApply(): Out with CRUD = { - out match - case c@Created(v) => node.state += (inLocation -> v); c - case u@Updated(_, v) => node.state += (inLocation -> v); u - case d: Deleted => node.state -= inLocation; d - case c: Commit => node.apply(c); c - case other => other - } - } -} \ No newline at end of file diff --git a/core/src/main/scala/dev/rudiments/hardcore/Root.scala b/core/src/main/scala/dev/rudiments/hardcore/Root.scala deleted file mode 100644 index 627a251c..00000000 --- a/core/src/main/scala/dev/rudiments/hardcore/Root.scala +++ /dev/null @@ -1,7 +0,0 @@ -package dev.rudiments.hardcore - -object Root { - private val node = Node.empty - - -} diff --git a/core/src/main/scala/dev/rudiments/hardcore/Table.scala b/core/src/main/scala/dev/rudiments/hardcore/Table.scala new file mode 100644 index 00000000..05ae509e --- /dev/null +++ b/core/src/main/scala/dev/rudiments/hardcore/Table.scala @@ -0,0 +1,101 @@ +package dev.rudiments.hardcore + +import scala.collection.mutable +import scala.reflect.ClassTag + +class Table[R : ClassTag, C, K : ClassTag]( + val schema: Seq[(C, Predicate)] = Seq.empty, + val log: mutable.Buffer[(K, CUD[K, R])] = mutable.Buffer.empty, + val state: mutable.Map[K, R] = mutable.Map.empty, +) { + + def read(where: K): Readen[R] | NotFound[K] = state.get(where) match { + case Some(found) => Readen(found) + case None => NotFound(where) + } + + def size: Int = state.size + + type Evt = CUD[K, R] + type Rep = CUDR[K, R] + + def whatIf(where: K, evt: Evt): Rep = (read(where), evt) match { + case (Readen(old), u@Updated(v1, v2)) if v1 == old => u + case (Readen(old), d@Deleted(v)) if v == old => d + case (Readen(old), m@Moved(v, to: K)) if v == old => read(to) match { + case _: NotFound[K] => m + case r: Readen[R] => Conflict(m, r) + } + case (_: NotFound[K], c: Created[R]) => c + case (readen, err) => Conflict(err, readen) + } + + def apply(where: K, what: Evt): Rep = whatIf(where, what) match { + case c@Created(v: R) => + log += (where -> c.asInstanceOf[Created[R]]) //can do it without .asInstanceOf ? + state += (where -> v) + c + case u@Updated(_, v: R) => + log += (where -> u.asInstanceOf[Updated[R]]) + state += (where -> v) + u + case d@Deleted(old: R) => + log += (where -> d.asInstanceOf[Deleted[R]]) + state -= where + d + case m@Moved(v: R, to: K) => + log += (where -> m.asInstanceOf[Moved[K, R]]) + state -= where + state += (to -> v) + m + case err: Report => err + case other => throw new IllegalArgumentException(s"Should never happen: $other") + } + + private val splitRep: PartialFunction[(K, Rep), Either[(K, Report), (K, Evt)]] = { + case (k, r: Report) => Left(k -> r) + case (k, e: Evt) => Right(k -> e) + case (k, other) => throw new IllegalArgumentException(s"Should never happen: $other") + } + + def apply(tx: Tx[K, R]): TxReport[K, R] = { + val branch = new Table[R, C, K](schema, mutable.Buffer.empty, this.state.clone()) + val (err: Seq[(K, Report)], oks: Seq[(K, Evt)]) = tx.events.partitionMap { (k, evt) => + splitRep(k -> branch.whatIf(k, evt)) + } + if(err.isEmpty) { + val report = oks.partitionMap { (k, evt) => splitRep(k -> this.apply(k, evt)) } + TxReport(report._2, report._1) + } else { + TxReport(Seq.empty, err) + } + } + + def create(where: K, what: R): Rep = this.apply(where, Created(what)) + def + (w: (K, R)): Rep = this.create(w._1, w._2) + + def update(where: K, to: R): Rep = read(where) match { + case Readen(old) => this.apply(where, Updated(old, to)) + case nf: NotFound[K] => nf + } + def * (w: (K, R)): Rep = this.update(w._1, w._2) + + def delete(where: K): Rep = read(where) match { + case Readen(old) => this.apply(where, Deleted(old)) + case nf: NotFound[K] => nf + } + def - (where: K): Rep = this.delete(where) + + def move(from: K, to: K): Rep = (read(from), read(to)) match { + case (Readen(old), nf: NotFound[K]) => this.apply(from, Moved(old, to)) + case (Readen(old), r@Readen(err)) => Conflict(Moved(old, to), r) + case (nf: NotFound[K], _) => nf + } + def >>(w: (K, K)): Rep = this.move(w._1, w._2) +} + +object Table { + def empty[R : ClassTag, K : ClassTag] = new Table[R, String, K]( + Seq.empty, mutable.Buffer.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 index 17f2c2e3..9d9d7fcd 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Thing.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Thing.scala @@ -1,16 +1,16 @@ package dev.rudiments.hardcore -sealed trait Thing extends Product {} +sealed trait Thing {} sealed trait Predicate extends Thing {} final case class Type( - fields: (ID, Field)* + fields: (String, Field)* ) extends Predicate { def data(values: Any*): Data = Data(this, values) } object Type { - def of(predicates: (ID, Predicate)*): Type = new Type( + def of(predicates: (String, Predicate)*): Type = new Type( predicates.map((id, p) => id -> Field(p, Required)) :_* ) } @@ -25,9 +25,11 @@ case object Required extends FieldKind case class WithDefault(d: Any) extends FieldKind object Optional extends WithDefault(None) +final class CustomPredicate[A](f: A => Boolean) extends Predicate {} + +case object Nothing extends Predicate {} + final case class Data( what: Predicate, data: Any -) extends Thing - -case object Nothing extends Predicate {} +) extends Thing \ No newline at end of file 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 bb018bea..00000000 --- a/core/src/main/scala/dev/rudiments/hardcore/Tx.scala +++ /dev/null @@ -1,11 +0,0 @@ -package dev.rudiments.hardcore - -import java.lang -import scala.collection.mutable - -case class Tx( - root: Node, - log: mutable.LinkedHashMap[Location, mutable.Seq[Out with CRUD]] -) { - -} diff --git a/core/src/main/scala/dev/rudiments/utils/SaltedMap.scala b/core/src/main/scala/dev/rudiments/utils/SaltedMap.scala index 9ee7dfcf..2169e6ef 100644 --- a/core/src/main/scala/dev/rudiments/utils/SaltedMap.scala +++ b/core/src/main/scala/dev/rudiments/utils/SaltedMap.scala @@ -1,7 +1,5 @@ package dev.rudiments.utils -import dev.rudiments.hardcore.Location - class SaltedMap[K, +V](values: Array[(K, V)]) extends Map[K, V]{ val salt: Int = 0;//TODO 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 c3471bf8..00000000 --- a/core/src/test/scala/test/dev/rudiments/hardcore/NodeSpec.scala +++ /dev/null @@ -1,60 +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 { - case class Something(a: String, i: Int) - private val s1 = Something("abc", 42) - private val s2 = Something("cde", 24) - - "Node" should { - val node: Node = Node.empty - "created empty" in { - node.size should be (0) - } - - "add something" in { - node >+ ID("42") -> s1 should be (Created(s1)) - node.size should be (1) - } - - "update something" in { - node >* ID("42") -> s2 should be(Updated(s1, s2)) - node.size should be(1) - } - - "delete something" in { - node >- ID("42") should be(Deleted(s2)) - node.size should be(0) - } - - "apply commit" in { - val pairs = (1 to 10).map (i => ID(i.toString) -> Created(Something(i.toHexString, i))) - node(Commit(pairs:_*)) should be(Commit(pairs:_*)) - node.size should be(10) - } - - "create nested node" in { - node >+ ID("n") -> Node.empty should be (Created(Node.empty)) - node.size should be(11) - } - - "put nested values" in { - val p = ID("n") / ID("123") - node >+ p -> s1 should be (Created(s1)) - node.size should be (11) - node.read(p) should be (Readen(s1)) - } - - "apply nested commits" in { - val pairs = (24 to 42).map (i => ID(i.toString) -> Created(Something(i.toHexString, i))) - node(Commit(ID("n") -> Commit(pairs:_*))) should be (Commit(ID("n") -> Commit(pairs:_*))) - node.state(ID("n")).asInstanceOf[Node].size should be (20) - } - } -} diff --git a/core/src/test/scala/test/dev/rudiments/hardcore/TableSpec.scala b/core/src/test/scala/test/dev/rudiments/hardcore/TableSpec.scala new file mode 100644 index 00000000..ee9d7db0 --- /dev/null +++ b/core/src/test/scala/test/dev/rudiments/hardcore/TableSpec.scala @@ -0,0 +1,66 @@ +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 TableSpec extends AnyWordSpec with Matchers { + case class Something(a: String, i: Int) + private val s1 = Something("abc", 42) + private val s2 = Something("cde", 24) + + private val t = Table.empty[Something, String] + + "Node" should { + "created empty" in { + t.size should be (0) + } + + "add something" in { + t + ("42" -> s1) should be (Created(s1)) + t.size should be (1) + } + + "update something" in { + t * ("42" -> s2) should be(Updated(s1, s2)) + t.size should be(1) + } + + "move something" in { + t >> ("42" -> "24") should be(Moved(s2, "24")) + t.size should be(1) + } + + "delete something" in { + t - "24" should be(Deleted(s2)) + t.size should be(0) + } + + "apply commit" in { + val pairs = (1 to 10).map (i => i.toString -> Created(Something(i.toHexString, i))) + t(Tx(pairs:_*)) should be(TxReport(pairs, Seq.empty)) + t.size should be(10) + } +// +// "create nested node" ignore { +// t >+ "n" -> Table.empty[Something, String] should be (Created(Node.empty)) +// t.size should be(11) +// } +// +// "put nested values" in { +// val p = ID("n") / ID("123") +// t >+ p -> s1 should be (Created(s1)) +// t.size should be (11) +// t.read(p) should be (Readen(s1)) +// } +// +// "apply nested commits" in { +// val pairs = (24 to 42).map (i => ID(i.toString) -> Created(Something(i.toHexString, i))) +// t(Commit(ID("n") -> Commit(pairs:_*))) should be (Commit(ID("n") -> Commit(pairs:_*))) +// t.state(ID("n")).asInstanceOf[Node].size should be (20) +// } + } +} From d634f1393e35a545f7f73859c4b5b37a6fa3fedf Mon Sep 17 00:00:00 2001 From: gennady Date: Wed, 7 Jun 2023 12:05:06 +0600 Subject: [PATCH 45/75] draft tables --- .../scala/dev/rudiments/hardcore/CRUD.scala | 67 ++++++++++++------- .../dev/rudiments/hardcore/Message.scala | 19 +++++- .../scala/dev/rudiments/hardcore/Table.scala | 50 ++------------ 3 files changed, 67 insertions(+), 69 deletions(-) diff --git a/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala b/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala index 63c19691..88a8cdb0 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala @@ -2,28 +2,45 @@ package dev.rudiments.hardcore import scala.collection.immutable.ListMap -sealed trait CRUD {} -object CRUD {} - -case class Create(value: Product) extends Command with CRUD -case class Read[K](where: K) extends Query with CRUD -case class Update(old: Product, value: Product) extends Command with CRUD -case class Delete(old: Product) extends Command with CRUD -case class Move[K](from: K, to: K) extends Command with CRUD - -case class Created[V](value: V) extends Event with CRUD -case class Readen[V](value: V) extends Report with CRUD -case class Updated[V](old: V, value: V) extends Event with CRUD -case class Deleted[V](old: V) extends Event with CRUD -case class Moved[K, V](value: V, to: K) extends Event with CRUD -case class Tx[K, V](events: (K, CUD[K, V])*) extends Event with CRUD - -case class NotFound[K](where: K) extends Report with CRUD -case object Identical extends Report with CRUD -case class TxReport[K, V](ok: Seq[(K, CUD[K, V])], errors: Seq[(K, Report)]) extends Report with CRUD -case class Conflict(in: Event, actual: Out) extends Error with CRUD -case class NotSupported(m: Message) extends Error with CRUD -case class InternalError(t: Throwable) extends Error with CRUD - -type CUD[K, V] = Created[V] | Updated[V] | Deleted[V] | Moved[K, V] -type CUDR[K, V] = CUD[K, V] | Report \ No newline at end of file +trait CRUD[K, V] { + type Evt = CUD[K, V] + type Rep = CUDR[K, V] + + def read(where: K): Readen[V] | NotFound[K] + def apply(where: K, what: Evt): Rep + def apply(tx: Tx[K, V]): TxReport[K, V] + def size: Int + + def whatIf(where: K, evt: Evt): Rep = (read(where), evt) match { + case (Readen(old), u@Updated(v1, v2)) if v1 == old => u + case (Readen(old), d@Deleted(v)) if v == old => d + case (Readen(old), m@Moved(v, to: K)) if v == old => read(to) match { + case _: NotFound[K] => m + case r: Readen[V] => Conflict(m, r) + } + case (_: NotFound[K], c: Created[V]) => c + case (readen, err) => Conflict(err, readen) + } + + def create(where: K, what: V): Rep = this.apply(where, Created(what)) + def +(w: (K, V)): Rep = this.create(w._1, w._2) + + def update(where: K, to: V): Rep = read(where) match { + case Readen(old) => this.apply(where, Updated(old, to)) + case nf: NotFound[K] => nf + } + def *(w: (K, V)): Rep = this.update(w._1, w._2) + + def delete(where: K): Rep = read(where) match { + case Readen(old) => this.apply(where, Deleted(old)) + case nf: NotFound[K] => nf + } + def -(where: K): Rep = this.delete(where) + + def move(from: K, to: K): Rep = (read(from), read(to)) match { + case (Readen(old), nf: NotFound[K]) => this.apply(from, Moved(old, to)) + case (Readen(old), r@Readen(err)) => Conflict(Moved(old, to), r) + case (nf: NotFound[K], _) => nf + } + def >>(w: (K, K)): Rep = this.move(w._1, w._2) +} diff --git a/core/src/main/scala/dev/rudiments/hardcore/Message.scala b/core/src/main/scala/dev/rudiments/hardcore/Message.scala index 7b2b0cd4..6d71a195 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Message.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Message.scala @@ -8,4 +8,21 @@ trait Command extends In trait Query extends In trait Event extends Out trait Report extends Out -trait Error extends Report \ No newline at end of file +trait Error extends Report + +case class Created[V](value: V) extends Event +case class Readen[V](value: V) extends Report +case class Updated[V](old: V, value: V) extends Event +case class Deleted[V](old: V) extends Event +case class Moved[K, V](value: V, to: K) extends Event +case class Tx[K, V](events: (K, CUD[K, V])*) extends Event + +case class NotFound[K](where: K) extends Report +case object Identical extends Report +case class TxReport[K, V](ok: Seq[(K, CUD[K, V])], errors: Seq[(K, Report)]) extends Report +case class Conflict(in: Event, actual: Out) extends Error +case class NotSupported(m: Message) extends Error +case class InternalError(t: Throwable) extends Error + +type CUD[K, V] = Created[V] | Updated[V] | Deleted[V] | Moved[K, V] +type CUDR[K, V] = CUD[K, V] | Report \ No newline at end of file diff --git a/core/src/main/scala/dev/rudiments/hardcore/Table.scala b/core/src/main/scala/dev/rudiments/hardcore/Table.scala index 05ae509e..ed7fb86e 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Table.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Table.scala @@ -3,34 +3,20 @@ package dev.rudiments.hardcore import scala.collection.mutable import scala.reflect.ClassTag -class Table[R : ClassTag, C, K : ClassTag]( +class Table[R : ClassTag, C, K : ClassTag] ( val schema: Seq[(C, Predicate)] = Seq.empty, val log: mutable.Buffer[(K, CUD[K, R])] = mutable.Buffer.empty, - val state: mutable.Map[K, R] = mutable.Map.empty, -) { + val state: mutable.Map[K, R] = mutable.Map.empty +) extends CRUD[K, R] { - def read(where: K): Readen[R] | NotFound[K] = state.get(where) match { + override def read(where: K): Readen[R] | NotFound[K] = state.get(where) match { case Some(found) => Readen(found) case None => NotFound(where) } - def size: Int = state.size + override def size: Int = state.size - type Evt = CUD[K, R] - type Rep = CUDR[K, R] - - def whatIf(where: K, evt: Evt): Rep = (read(where), evt) match { - case (Readen(old), u@Updated(v1, v2)) if v1 == old => u - case (Readen(old), d@Deleted(v)) if v == old => d - case (Readen(old), m@Moved(v, to: K)) if v == old => read(to) match { - case _: NotFound[K] => m - case r: Readen[R] => Conflict(m, r) - } - case (_: NotFound[K], c: Created[R]) => c - case (readen, err) => Conflict(err, readen) - } - - def apply(where: K, what: Evt): Rep = whatIf(where, what) match { + override def apply(where: K, what: Evt): Rep = whatIf(where, what) match { case c@Created(v: R) => log += (where -> c.asInstanceOf[Created[R]]) //can do it without .asInstanceOf ? state += (where -> v) @@ -58,7 +44,7 @@ class Table[R : ClassTag, C, K : ClassTag]( case (k, other) => throw new IllegalArgumentException(s"Should never happen: $other") } - def apply(tx: Tx[K, R]): TxReport[K, R] = { + override def apply(tx: Tx[K, R]): TxReport[K, R] = { val branch = new Table[R, C, K](schema, mutable.Buffer.empty, this.state.clone()) val (err: Seq[(K, Report)], oks: Seq[(K, Evt)]) = tx.events.partitionMap { (k, evt) => splitRep(k -> branch.whatIf(k, evt)) @@ -70,28 +56,6 @@ class Table[R : ClassTag, C, K : ClassTag]( TxReport(Seq.empty, err) } } - - def create(where: K, what: R): Rep = this.apply(where, Created(what)) - def + (w: (K, R)): Rep = this.create(w._1, w._2) - - def update(where: K, to: R): Rep = read(where) match { - case Readen(old) => this.apply(where, Updated(old, to)) - case nf: NotFound[K] => nf - } - def * (w: (K, R)): Rep = this.update(w._1, w._2) - - def delete(where: K): Rep = read(where) match { - case Readen(old) => this.apply(where, Deleted(old)) - case nf: NotFound[K] => nf - } - def - (where: K): Rep = this.delete(where) - - def move(from: K, to: K): Rep = (read(from), read(to)) match { - case (Readen(old), nf: NotFound[K]) => this.apply(from, Moved(old, to)) - case (Readen(old), r@Readen(err)) => Conflict(Moved(old, to), r) - case (nf: NotFound[K], _) => nf - } - def >>(w: (K, K)): Rep = this.move(w._1, w._2) } object Table { From 846075900401fdec3c2c62b8f1e9d83cb9069286 Mon Sep 17 00:00:00 2001 From: gennady Date: Wed, 7 Jun 2023 23:31:10 +0600 Subject: [PATCH 46/75] draft codecs --- .../dev/rudiments/codecs/BinaryDecoder.scala | 45 +++++++++++++++ .../dev/rudiments/codecs/BinaryEncoder.scala | 57 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 codecs/src/main/scala/dev/rudiments/codecs/BinaryDecoder.scala create mode 100644 codecs/src/main/scala/dev/rudiments/codecs/BinaryEncoder.scala diff --git a/codecs/src/main/scala/dev/rudiments/codecs/BinaryDecoder.scala b/codecs/src/main/scala/dev/rudiments/codecs/BinaryDecoder.scala new file mode 100644 index 00000000..7ba1a933 --- /dev/null +++ b/codecs/src/main/scala/dev/rudiments/codecs/BinaryDecoder.scala @@ -0,0 +1,45 @@ +package dev.rudiments.codecs + +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets.UTF_8 + +trait BinaryDecoder[A] { + extension (arr: Array[Byte]) def decode(): A +} + +object BinaryDecoder { + given intDecoder: BinaryDecoder[Int] with + extension (arr: Array[Byte]) + def decode(): Int = ByteBuffer.wrap(arr).getInt + + given longDecoder: BinaryDecoder[Long] with + extension (arr: Array[Byte]) + def decode(): Long = ByteBuffer.wrap(arr).getLong() + + given stringDecoder: BinaryDecoder[String] with + extension (arr: Array[Byte]) + def decode(): String = { + val buff = ByteBuffer.wrap(arr) + val size = buff.getInt + val a: Array[Byte] = new Array(size) + buff.get(a) + new String(a, UTF_8) + } + + given optionDecoder[T: BinaryDecoder]: BinaryDecoder[Option[T]] with + extension (arr: Array[Byte]) + def decode(): Option[T] = { + if (arr.isEmpty) None + else arr.decode() + } + + given iterableDecoder[T: BinaryDecoder]: BinaryDecoder[Iterable[T]] with + extension (arr: Array[Byte]) + def decode(): Iterable[T] = { + val buff = ByteBuffer.wrap(arr) + val size = buff.getInt + val a: Array[Byte] = new Array(size) + ??? + + } +} \ No newline at end of file diff --git a/codecs/src/main/scala/dev/rudiments/codecs/BinaryEncoder.scala b/codecs/src/main/scala/dev/rudiments/codecs/BinaryEncoder.scala new file mode 100644 index 00000000..9f9b11c9 --- /dev/null +++ b/codecs/src/main/scala/dev/rudiments/codecs/BinaryEncoder.scala @@ -0,0 +1,57 @@ +package dev.rudiments.codecs + +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets.UTF_8 + +trait BinaryEncoder[A] { + extension (a: A) def encode(): Array[Byte] +} + +object BinaryEncoder { + given intEncoder: BinaryEncoder[Int] with + extension (a: Int) + def encode(): Array[Byte] = ByteBuffer.allocate(4).putInt(a).array() + + given longEncoder: BinaryEncoder[Long] with + extension (a: Long) + def encode(): Array[Byte] = ByteBuffer.allocate(8).putLong(a).array() + + given stringEncoder: BinaryEncoder[String] with + extension (a: String) + def encode(): Array[Byte] = { + val arr = a.getBytes(UTF_8) + ByteBuffer.allocate(arr.size + 4) + .putInt(arr.size) + .put(arr) + .array() + } + + given optionEncoder[T: BinaryEncoder]: BinaryEncoder[Option[T]] with + extension (a: Option[T]) + def encode(): Array[Byte] = a match + case Some(v) => v.encode() + case None => Array.empty[Byte] + + given product2Encoder[A: BinaryEncoder, B: BinaryEncoder]: BinaryEncoder[Product2[A, B]] with + extension (p: Product2[A, B]) + def encode(): Array[Byte] = { + val a = p._1.encode() + val b = p._2.encode() + a ++ b + } + + //TODO study io.circe.EncoderDerivation & io.circe.Derivation#summonEncodersRec + given tuple2Encoder[A: BinaryEncoder, B: BinaryEncoder]: BinaryEncoder[(A, B)] with + extension (t: (A, B)) + def encode(): Array[Byte] = t._1.encode() ++ t._2.encode() + + given seqEncoder[T: BinaryEncoder]: BinaryEncoder[Seq[T]] with + extension (a: Seq[T]) + def encode(): Array[Byte] = { + val arr = a.foldLeft(Seq.empty[Byte]) { (acc, el) => acc ++ el.encode() } + ByteBuffer.allocate(arr.size + 4) + .putInt(arr.size) + .put(arr.toArray[Byte]) + .array() + } +} From 7f613db10d169fca7f47e44c321d99158c9f84d4 Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 8 Jun 2023 00:03:32 +0600 Subject: [PATCH 47/75] drop redundant codecs module --- codecs/build.gradle | 6 ------ codecs/src/test/resources/application.conf | 14 -------------- codecs/src/test/resources/logback.xml | 14 -------------- core/build.gradle | 3 +++ .../scala/dev/rudiments/codecs/BinaryDecoder.scala | 0 .../scala/dev/rudiments/codecs/BinaryEncoder.scala | 0 .../scala/dev/rudiments/codecs/BytesCodec.scala | 0 .../test/dev/rudiments/codecs/CirceTest.scala | 5 +++-- example/build.gradle | 1 - file/build.gradle | 1 - settings.gradle | 1 - 11 files changed, 6 insertions(+), 39 deletions(-) delete mode 100644 codecs/build.gradle delete mode 100644 codecs/src/test/resources/application.conf delete mode 100644 codecs/src/test/resources/logback.xml rename {codecs => core}/src/main/scala/dev/rudiments/codecs/BinaryDecoder.scala (100%) rename {codecs => core}/src/main/scala/dev/rudiments/codecs/BinaryEncoder.scala (100%) rename {codecs => core}/src/main/scala/dev/rudiments/codecs/BytesCodec.scala (100%) rename {codecs => core}/src/test/scala/test/dev/rudiments/codecs/CirceTest.scala (99%) diff --git a/codecs/build.gradle b/codecs/build.gradle deleted file mode 100644 index b543e0f7..00000000 --- a/codecs/build.gradle +++ /dev/null @@ -1,6 +0,0 @@ -dependencies { - implementation project(':core') - - 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/codecs/src/test/resources/application.conf b/codecs/src/test/resources/application.conf deleted file mode 100644 index cd6a8ae3..00000000 --- a/codecs/src/test/resources/application.conf +++ /dev/null @@ -1,14 +0,0 @@ -http { - 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/codecs/src/test/resources/logback.xml b/codecs/src/test/resources/logback.xml deleted file mode 100644 index a994bdfc..00000000 --- a/codecs/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/core/build.gradle b/core/build.gradle index cf22ba9f..dc540ace 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -1,3 +1,6 @@ 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/codecs/src/main/scala/dev/rudiments/codecs/BinaryDecoder.scala b/core/src/main/scala/dev/rudiments/codecs/BinaryDecoder.scala similarity index 100% rename from codecs/src/main/scala/dev/rudiments/codecs/BinaryDecoder.scala rename to core/src/main/scala/dev/rudiments/codecs/BinaryDecoder.scala diff --git a/codecs/src/main/scala/dev/rudiments/codecs/BinaryEncoder.scala b/core/src/main/scala/dev/rudiments/codecs/BinaryEncoder.scala similarity index 100% rename from codecs/src/main/scala/dev/rudiments/codecs/BinaryEncoder.scala rename to core/src/main/scala/dev/rudiments/codecs/BinaryEncoder.scala diff --git a/codecs/src/main/scala/dev/rudiments/codecs/BytesCodec.scala b/core/src/main/scala/dev/rudiments/codecs/BytesCodec.scala similarity index 100% rename from codecs/src/main/scala/dev/rudiments/codecs/BytesCodec.scala rename to core/src/main/scala/dev/rudiments/codecs/BytesCodec.scala diff --git a/codecs/src/test/scala/test/dev/rudiments/codecs/CirceTest.scala b/core/src/test/scala/test/dev/rudiments/codecs/CirceTest.scala similarity index 99% rename from codecs/src/test/scala/test/dev/rudiments/codecs/CirceTest.scala rename to core/src/test/scala/test/dev/rudiments/codecs/CirceTest.scala index 9a2fd7b2..b84974c1 100644 --- a/codecs/src/test/scala/test/dev/rudiments/codecs/CirceTest.scala +++ b/core/src/test/scala/test/dev/rudiments/codecs/CirceTest.scala @@ -1,12 +1,13 @@ package test.dev.rudiments.codecs -import io.circe.{Codec, Json} -import io.circe.generic.semiauto.* import org.junit.runner.RunWith import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec import org.scalatestplus.junit.JUnitRunner +import io.circe.generic.semiauto.* +import io.circe.{Codec, Json} + @RunWith(classOf[JUnitRunner]) class CirceTest extends AnyWordSpec with Matchers { case class Sample( diff --git a/example/build.gradle b/example/build.gradle index 7a9244a4..4bd26c20 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -1,6 +1,5 @@ dependencies { implementation project(':core') - implementation project(':codecs') implementation project(':file') implementation project(':git') diff --git a/file/build.gradle b/file/build.gradle index 09d68f1a..e4dbb7fe 100644 --- a/file/build.gradle +++ b/file/build.gradle @@ -1,4 +1,3 @@ dependencies { implementation project(':core') - implementation project(':codecs') } diff --git a/settings.gradle b/settings.gradle index bd6aa5b7..0c00cb08 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,5 @@ rootProject.name = 'hardcore' include 'core' -include 'codecs' include 'file' include 'git' include 'example' From 397ebcc83c47a473241917f2431729cb5a660714 Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 8 Jun 2023 02:40:48 +0600 Subject: [PATCH 48/75] refactor --- .../hardcore/{CRUD.scala => Store.scala} | 14 ++++--- .../scala/dev/rudiments/hardcore/Table.scala | 38 ++++++++++--------- .../dev/rudiments/hardcore/TableSpec.scala | 2 +- 3 files changed, 29 insertions(+), 25 deletions(-) rename core/src/main/scala/dev/rudiments/hardcore/{CRUD.scala => Store.scala} (74%) diff --git a/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala b/core/src/main/scala/dev/rudiments/hardcore/Store.scala similarity index 74% rename from core/src/main/scala/dev/rudiments/hardcore/CRUD.scala rename to core/src/main/scala/dev/rudiments/hardcore/Store.scala index 88a8cdb0..5ffd1fad 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/CRUD.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Store.scala @@ -2,7 +2,7 @@ package dev.rudiments.hardcore import scala.collection.immutable.ListMap -trait CRUD[K, V] { +trait Store[K, V] { type Evt = CUD[K, V] type Rep = CUDR[K, V] @@ -11,33 +11,35 @@ trait CRUD[K, V] { def apply(tx: Tx[K, V]): TxReport[K, V] def size: Int - def whatIf(where: K, evt: Evt): Rep = (read(where), evt) match { + def whatIf(where: K, evt: Evt): Rep = (this.read(where), evt) match { case (Readen(old), u@Updated(v1, v2)) if v1 == old => u case (Readen(old), d@Deleted(v)) if v == old => d - case (Readen(old), m@Moved(v, to: K)) if v == old => read(to) match { + case (Readen(old), m@Moved(v, to: K)) if v == old => this.read(to) match { case _: NotFound[K] => m case r: Readen[V] => Conflict(m, r) } case (_: NotFound[K], c: Created[V]) => c case (readen, err) => Conflict(err, readen) } +} +trait CRUD[K, V] extends Store[K, V] { def create(where: K, what: V): Rep = this.apply(where, Created(what)) def +(w: (K, V)): Rep = this.create(w._1, w._2) - def update(where: K, to: V): Rep = read(where) match { + def update(where: K, to: V): Rep = this.read(where) match { case Readen(old) => this.apply(where, Updated(old, to)) case nf: NotFound[K] => nf } def *(w: (K, V)): Rep = this.update(w._1, w._2) - def delete(where: K): Rep = read(where) match { + def delete(where: K): Rep = this.read(where) match { case Readen(old) => this.apply(where, Deleted(old)) case nf: NotFound[K] => nf } def -(where: K): Rep = this.delete(where) - def move(from: K, to: K): Rep = (read(from), read(to)) match { + def move(from: K, to: K): Rep = (this.read(from), this.read(to)) match { case (Readen(old), nf: NotFound[K]) => this.apply(from, Moved(old, to)) case (Readen(old), r@Readen(err)) => Conflict(Moved(old, to), r) case (nf: NotFound[K], _) => nf diff --git a/core/src/main/scala/dev/rudiments/hardcore/Table.scala b/core/src/main/scala/dev/rudiments/hardcore/Table.scala index ed7fb86e..be302120 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Table.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Table.scala @@ -3,13 +3,13 @@ package dev.rudiments.hardcore import scala.collection.mutable import scala.reflect.ClassTag -class Table[R : ClassTag, C, K : ClassTag] ( - val schema: Seq[(C, Predicate)] = Seq.empty, - val log: mutable.Buffer[(K, CUD[K, R])] = mutable.Buffer.empty, - val state: mutable.Map[K, R] = mutable.Map.empty -) extends CRUD[K, R] { +class Table[K : ClassTag, V : ClassTag] ( + val schema: Predicate = Nothing, + val log: mutable.Buffer[(K, CUD[K, V])] = mutable.Buffer.empty, + val state: mutable.Map[K, V] = mutable.Map.empty +) extends CRUD[K, V] { - override def read(where: K): Readen[R] | NotFound[K] = state.get(where) match { + override def read(where: K): Readen[V] | NotFound[K] = state.get(where) match { case Some(found) => Readen(found) case None => NotFound(where) } @@ -17,20 +17,20 @@ class Table[R : ClassTag, C, K : ClassTag] ( override def size: Int = state.size override def apply(where: K, what: Evt): Rep = whatIf(where, what) match { - case c@Created(v: R) => - log += (where -> c.asInstanceOf[Created[R]]) //can do it without .asInstanceOf ? + case c@Created(v: V) => + log += (where -> c.asInstanceOf[Created[V]]) //can do it without .asInstanceOf ? state += (where -> v) c - case u@Updated(_, v: R) => - log += (where -> u.asInstanceOf[Updated[R]]) + case u@Updated(_, v: V) => + log += (where -> u.asInstanceOf[Updated[V]]) state += (where -> v) u - case d@Deleted(old: R) => - log += (where -> d.asInstanceOf[Deleted[R]]) + case d@Deleted(old: V) => + log += (where -> d.asInstanceOf[Deleted[V]]) state -= where d - case m@Moved(v: R, to: K) => - log += (where -> m.asInstanceOf[Moved[K, R]]) + case m@Moved(v: V, to: K) => + log += (where -> m.asInstanceOf[Moved[K, V]]) state -= where state += (to -> v) m @@ -44,8 +44,8 @@ class Table[R : ClassTag, C, K : ClassTag] ( case (k, other) => throw new IllegalArgumentException(s"Should never happen: $other") } - override def apply(tx: Tx[K, R]): TxReport[K, R] = { - val branch = new Table[R, C, K](schema, mutable.Buffer.empty, this.state.clone()) + override def apply(tx: Tx[K, V]): TxReport[K, V] = { + val branch = this.branch val (err: Seq[(K, Report)], oks: Seq[(K, Evt)]) = tx.events.partitionMap { (k, evt) => splitRep(k -> branch.whatIf(k, evt)) } @@ -56,10 +56,12 @@ class Table[R : ClassTag, C, K : ClassTag] ( TxReport(Seq.empty, err) } } + + def branch = new Table[K, V](schema, mutable.Buffer.empty, this.state.clone()) } object Table { - def empty[R : ClassTag, K : ClassTag] = new Table[R, String, K]( - Seq.empty, mutable.Buffer.empty, mutable.Map.empty + def empty[K : ClassTag, V : ClassTag] = new Table[K, V]( + Nothing, mutable.Buffer.empty, mutable.Map.empty ) } diff --git a/core/src/test/scala/test/dev/rudiments/hardcore/TableSpec.scala b/core/src/test/scala/test/dev/rudiments/hardcore/TableSpec.scala index ee9d7db0..cfc9d123 100644 --- a/core/src/test/scala/test/dev/rudiments/hardcore/TableSpec.scala +++ b/core/src/test/scala/test/dev/rudiments/hardcore/TableSpec.scala @@ -12,7 +12,7 @@ class TableSpec extends AnyWordSpec with Matchers { private val s1 = Something("abc", 42) private val s2 = Something("cde", 24) - private val t = Table.empty[Something, String] + private val t = Table.empty[String, Something] "Node" should { "created empty" in { From 79bdc5b38b6976b047e03329ad18a168b43ec43c Mon Sep 17 00:00:00 2001 From: gennady Date: Tue, 29 Aug 2023 07:46:39 +0200 Subject: [PATCH 49/75] try again --- .../scala/dev/rudiments/hardcore/Memory.scala | 72 +++++++++++++++++++ .../dev/rudiments/hardcore/Message.scala | 21 +++--- .../scala/dev/rudiments/hardcore/Store.scala | 48 ------------- .../scala/dev/rudiments/hardcore/Table.scala | 67 ----------------- .../scala/dev/rudiments/hardcore/Thing.scala | 35 --------- .../dev/rudiments/hardcore/MemorySpec.scala | 35 +++++++++ .../dev/rudiments/hardcore/TableSpec.scala | 66 ----------------- 7 files changed, 118 insertions(+), 226 deletions(-) create mode 100644 core/src/main/scala/dev/rudiments/hardcore/Memory.scala delete mode 100644 core/src/main/scala/dev/rudiments/hardcore/Store.scala delete mode 100644 core/src/main/scala/dev/rudiments/hardcore/Table.scala delete mode 100644 core/src/main/scala/dev/rudiments/hardcore/Thing.scala create mode 100644 core/src/test/scala/test/dev/rudiments/hardcore/MemorySpec.scala delete mode 100644 core/src/test/scala/test/dev/rudiments/hardcore/TableSpec.scala diff --git a/core/src/main/scala/dev/rudiments/hardcore/Memory.scala b/core/src/main/scala/dev/rudiments/hardcore/Memory.scala new file mode 100644 index 00000000..3c928b22 --- /dev/null +++ b/core/src/main/scala/dev/rudiments/hardcore/Memory.scala @@ -0,0 +1,72 @@ +package dev.rudiments.hardcore + +import scala.collection.mutable +import scala.reflect.ClassTag + +class Memory { // global context + //TODO nodes + schemas = structure + //TODO types + import Location.* + val values: mutable.Map[ID, Any] = mutable.Map.empty + + def read(key: Location): Readen | NotFound = { + key match { + case id: ID => values.get(id) match { + case Some(v) => Readen(v) + case None => NotFound(id) + } + case p: Path => NotFound(p) + } + } + + def create(key: Location, value: Any): Response = key match { + case id: ID => values.get(id) match { + case Some(v) => Conflict(Created(v), Readen(v)) + case None => values += id -> value; Created(value) + } + case p: Path => NotFound(p) + } + def update(key: Location, oldValue: Any, newValue: Any): Response = key match { + case id: ID => values.get(id) match { + case Some(v) if v == oldValue => values += id -> newValue; Updated(v, newValue) + case Some(v) => Conflict(Updated(v, newValue), Readen(v)) + case None => NotFound(id) + } + case p: Path => NotFound(p) + } + def delete(key: Location, oldValue: Any): Response = key match { + case id: ID => values.get(id) match { + case Some(v) if v == oldValue => values -= id; Deleted(v) + case Some(v) => Conflict(Deleted(oldValue), Readen(v)) + case None => NotFound(id) + } + case p: Path => NotFound(p) + } + + + def /(s: String): Reading = new Reading(this, ID(s)) + def /?(s: String): Response = this.read(ID(s)) + def -(s: String): Response = { + val k = ID(s) + this.read(k) match { + case nf@NotFound(_) => nf + case Readen(r) => this.delete(k, r) + } + } +} + +class Reading(mem: Memory, key: Location) { + def +(value: Any) : Response = mem.read(key) match { + case NotFound(_) => mem.create(key, value) + case r => Conflict(Created(value), r) + } + def *(value: Any): Response = mem.read(key) match { + case nf@NotFound(_) => nf + case Readen(r) => mem.update(key, r, value) + } +} + +enum Location { + case ID(s: String) + case Path(ids: Seq[String]) +} \ No newline at end of file diff --git a/core/src/main/scala/dev/rudiments/hardcore/Message.scala b/core/src/main/scala/dev/rudiments/hardcore/Message.scala index 6d71a195..1e9b0be7 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Message.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Message.scala @@ -10,19 +10,20 @@ trait Event extends Out trait Report extends Out trait Error extends Report -case class Created[V](value: V) extends Event -case class Readen[V](value: V) extends Report -case class Updated[V](old: V, value: V) extends Event -case class Deleted[V](old: V) extends Event -case class Moved[K, V](value: V, to: K) extends Event -case class Tx[K, V](events: (K, CUD[K, V])*) extends Event +case class Create(value: Any) extends Command +case class Read(where: Location) extends Query +case class Update(old: Any, value: Any) extends Command +case class Delete(old: Any) extends Command -case class NotFound[K](where: K) extends Report +case class Created(value: Any) extends Event +case class Readen(value: Any) extends Report +case class Updated(old: Any, value: Any) extends Event +case class Deleted(old: Any) extends Event + +case class NotFound(where: Location) extends Report case object Identical extends Report -case class TxReport[K, V](ok: Seq[(K, CUD[K, V])], errors: Seq[(K, Report)]) extends Report case class Conflict(in: Event, actual: Out) extends Error case class NotSupported(m: Message) extends Error case class InternalError(t: Throwable) extends Error -type CUD[K, V] = Created[V] | Updated[V] | Deleted[V] | Moved[K, V] -type CUDR[K, V] = CUD[K, V] | Report \ No newline at end of file +type Response = Event | Report \ No newline at end of file diff --git a/core/src/main/scala/dev/rudiments/hardcore/Store.scala b/core/src/main/scala/dev/rudiments/hardcore/Store.scala deleted file mode 100644 index 5ffd1fad..00000000 --- a/core/src/main/scala/dev/rudiments/hardcore/Store.scala +++ /dev/null @@ -1,48 +0,0 @@ -package dev.rudiments.hardcore - -import scala.collection.immutable.ListMap - -trait Store[K, V] { - type Evt = CUD[K, V] - type Rep = CUDR[K, V] - - def read(where: K): Readen[V] | NotFound[K] - def apply(where: K, what: Evt): Rep - def apply(tx: Tx[K, V]): TxReport[K, V] - def size: Int - - def whatIf(where: K, evt: Evt): Rep = (this.read(where), evt) match { - case (Readen(old), u@Updated(v1, v2)) if v1 == old => u - case (Readen(old), d@Deleted(v)) if v == old => d - case (Readen(old), m@Moved(v, to: K)) if v == old => this.read(to) match { - case _: NotFound[K] => m - case r: Readen[V] => Conflict(m, r) - } - case (_: NotFound[K], c: Created[V]) => c - case (readen, err) => Conflict(err, readen) - } -} - -trait CRUD[K, V] extends Store[K, V] { - def create(where: K, what: V): Rep = this.apply(where, Created(what)) - def +(w: (K, V)): Rep = this.create(w._1, w._2) - - def update(where: K, to: V): Rep = this.read(where) match { - case Readen(old) => this.apply(where, Updated(old, to)) - case nf: NotFound[K] => nf - } - def *(w: (K, V)): Rep = this.update(w._1, w._2) - - def delete(where: K): Rep = this.read(where) match { - case Readen(old) => this.apply(where, Deleted(old)) - case nf: NotFound[K] => nf - } - def -(where: K): Rep = this.delete(where) - - def move(from: K, to: K): Rep = (this.read(from), this.read(to)) match { - case (Readen(old), nf: NotFound[K]) => this.apply(from, Moved(old, to)) - case (Readen(old), r@Readen(err)) => Conflict(Moved(old, to), r) - case (nf: NotFound[K], _) => nf - } - def >>(w: (K, K)): Rep = this.move(w._1, w._2) -} diff --git a/core/src/main/scala/dev/rudiments/hardcore/Table.scala b/core/src/main/scala/dev/rudiments/hardcore/Table.scala deleted file mode 100644 index be302120..00000000 --- a/core/src/main/scala/dev/rudiments/hardcore/Table.scala +++ /dev/null @@ -1,67 +0,0 @@ -package dev.rudiments.hardcore - -import scala.collection.mutable -import scala.reflect.ClassTag - -class Table[K : ClassTag, V : ClassTag] ( - val schema: Predicate = Nothing, - val log: mutable.Buffer[(K, CUD[K, V])] = mutable.Buffer.empty, - val state: mutable.Map[K, V] = mutable.Map.empty -) extends CRUD[K, V] { - - override def read(where: K): Readen[V] | NotFound[K] = state.get(where) match { - case Some(found) => Readen(found) - case None => NotFound(where) - } - - override def size: Int = state.size - - override def apply(where: K, what: Evt): Rep = whatIf(where, what) match { - case c@Created(v: V) => - log += (where -> c.asInstanceOf[Created[V]]) //can do it without .asInstanceOf ? - state += (where -> v) - c - case u@Updated(_, v: V) => - log += (where -> u.asInstanceOf[Updated[V]]) - state += (where -> v) - u - case d@Deleted(old: V) => - log += (where -> d.asInstanceOf[Deleted[V]]) - state -= where - d - case m@Moved(v: V, to: K) => - log += (where -> m.asInstanceOf[Moved[K, V]]) - state -= where - state += (to -> v) - m - case err: Report => err - case other => throw new IllegalArgumentException(s"Should never happen: $other") - } - - private val splitRep: PartialFunction[(K, Rep), Either[(K, Report), (K, Evt)]] = { - case (k, r: Report) => Left(k -> r) - case (k, e: Evt) => Right(k -> e) - case (k, other) => throw new IllegalArgumentException(s"Should never happen: $other") - } - - override def apply(tx: Tx[K, V]): TxReport[K, V] = { - val branch = this.branch - val (err: Seq[(K, Report)], oks: Seq[(K, Evt)]) = tx.events.partitionMap { (k, evt) => - splitRep(k -> branch.whatIf(k, evt)) - } - if(err.isEmpty) { - val report = oks.partitionMap { (k, evt) => splitRep(k -> this.apply(k, evt)) } - TxReport(report._2, report._1) - } else { - TxReport(Seq.empty, err) - } - } - - def branch = new Table[K, V](schema, mutable.Buffer.empty, this.state.clone()) -} - -object Table { - def empty[K : ClassTag, V : ClassTag] = new Table[K, V]( - Nothing, mutable.Buffer.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 9d9d7fcd..00000000 --- a/core/src/main/scala/dev/rudiments/hardcore/Thing.scala +++ /dev/null @@ -1,35 +0,0 @@ -package dev.rudiments.hardcore - -sealed trait Thing {} - -sealed trait Predicate extends Thing {} - -final case class Type( - fields: (String, Field)* -) extends Predicate { - def data(values: Any*): Data = Data(this, values) -} -object Type { - def of(predicates: (String, Predicate)*): Type = new Type( - predicates.map((id, p) => id -> Field(p, Required)) :_* - ) -} - -final case class Field( - spec: Predicate, - kind: FieldKind = Required -) extends Thing - -sealed trait FieldKind {} -case object Required extends FieldKind -case class WithDefault(d: Any) extends FieldKind -object Optional extends WithDefault(None) - -final class CustomPredicate[A](f: A => Boolean) extends Predicate {} - -case object Nothing extends Predicate {} - -final case class Data( - what: Predicate, - data: Any -) extends Thing \ No newline at end of file diff --git a/core/src/test/scala/test/dev/rudiments/hardcore/MemorySpec.scala b/core/src/test/scala/test/dev/rudiments/hardcore/MemorySpec.scala new file mode 100644 index 00000000..a38639e9 --- /dev/null +++ b/core/src/test/scala/test/dev/rudiments/hardcore/MemorySpec.scala @@ -0,0 +1,35 @@ +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{ + case class Something(a: Int, b: String) + + "Memory" should { + import Location._ + given mem: Memory = new Memory + val sample = Something(42, "forty two") + val sample2 = Something(43, "forty three") + + "read and create in the location" in { + mem /? "42" should be (NotFound(ID("42"))) + mem / "42" + sample should be (Created(sample)) + mem /? "42" should be (Readen(sample)) + } + + "update in the location" in { + mem / "42" * sample2 should be (Updated(sample, sample2)) + mem /? "42" should be (Readen(sample2)) + } + + "delete in the location" in { + mem - "42" should be (Deleted(sample2)) + mem /? "42" should be (NotFound(ID("42"))) + } + } +} diff --git a/core/src/test/scala/test/dev/rudiments/hardcore/TableSpec.scala b/core/src/test/scala/test/dev/rudiments/hardcore/TableSpec.scala deleted file mode 100644 index cfc9d123..00000000 --- a/core/src/test/scala/test/dev/rudiments/hardcore/TableSpec.scala +++ /dev/null @@ -1,66 +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 TableSpec extends AnyWordSpec with Matchers { - case class Something(a: String, i: Int) - private val s1 = Something("abc", 42) - private val s2 = Something("cde", 24) - - private val t = Table.empty[String, Something] - - "Node" should { - "created empty" in { - t.size should be (0) - } - - "add something" in { - t + ("42" -> s1) should be (Created(s1)) - t.size should be (1) - } - - "update something" in { - t * ("42" -> s2) should be(Updated(s1, s2)) - t.size should be(1) - } - - "move something" in { - t >> ("42" -> "24") should be(Moved(s2, "24")) - t.size should be(1) - } - - "delete something" in { - t - "24" should be(Deleted(s2)) - t.size should be(0) - } - - "apply commit" in { - val pairs = (1 to 10).map (i => i.toString -> Created(Something(i.toHexString, i))) - t(Tx(pairs:_*)) should be(TxReport(pairs, Seq.empty)) - t.size should be(10) - } -// -// "create nested node" ignore { -// t >+ "n" -> Table.empty[Something, String] should be (Created(Node.empty)) -// t.size should be(11) -// } -// -// "put nested values" in { -// val p = ID("n") / ID("123") -// t >+ p -> s1 should be (Created(s1)) -// t.size should be (11) -// t.read(p) should be (Readen(s1)) -// } -// -// "apply nested commits" in { -// val pairs = (24 to 42).map (i => ID(i.toString) -> Created(Something(i.toHexString, i))) -// t(Commit(ID("n") -> Commit(pairs:_*))) should be (Commit(ID("n") -> Commit(pairs:_*))) -// t.state(ID("n")).asInstanceOf[Node].size should be (20) -// } - } -} From a922224ef942db2f8f122cd18c980fe7fa030eaf Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 26 Oct 2023 13:47:16 +0400 Subject: [PATCH 50/75] drop old core, add codecs --- .../dev/rudiments/codecs/BinaryDecoder.scala | 45 ---------- .../dev/rudiments/codecs/BinaryEncoder.scala | 57 ------------- .../dev/rudiments/codecs/BytesCodec.scala | 26 ------ .../scala/dev/rudiments/codecs/Codec.scala | 41 +++++++++ .../scala/dev/rudiments/hardcore/Memory.scala | 72 ---------------- .../dev/rudiments/hardcore/Message.scala | 29 ------- .../scala/dev/rudiments/utils/SaltedMap.scala | 56 ------------- .../dev/rudiments/hardcore/MemorySpec.scala | 35 -------- .../dev/rudiments/utils/SaltedMapTest.scala | 20 ----- .../main/scala/dev/rudiments/file/About.scala | 43 ---------- .../scala/dev/rudiments/file/Repository.scala | 83 ------------------- .../main/scala/dev/rudiments/file/Tx.scala | 69 --------------- .../test/dev/rudiments/file/FileTest.scala | 42 ---------- 13 files changed, 41 insertions(+), 577 deletions(-) delete mode 100644 core/src/main/scala/dev/rudiments/codecs/BinaryDecoder.scala delete mode 100644 core/src/main/scala/dev/rudiments/codecs/BinaryEncoder.scala delete mode 100644 core/src/main/scala/dev/rudiments/codecs/BytesCodec.scala create mode 100644 core/src/main/scala/dev/rudiments/codecs/Codec.scala delete mode 100644 core/src/main/scala/dev/rudiments/hardcore/Memory.scala delete mode 100644 core/src/main/scala/dev/rudiments/hardcore/Message.scala delete mode 100644 core/src/main/scala/dev/rudiments/utils/SaltedMap.scala delete mode 100644 core/src/test/scala/test/dev/rudiments/hardcore/MemorySpec.scala delete mode 100644 core/src/test/scala/test/dev/rudiments/utils/SaltedMapTest.scala delete mode 100644 file/src/main/scala/dev/rudiments/file/About.scala delete mode 100644 file/src/main/scala/dev/rudiments/file/Repository.scala delete mode 100644 file/src/main/scala/dev/rudiments/file/Tx.scala delete mode 100644 file/src/test/scala/test/dev/rudiments/file/FileTest.scala diff --git a/core/src/main/scala/dev/rudiments/codecs/BinaryDecoder.scala b/core/src/main/scala/dev/rudiments/codecs/BinaryDecoder.scala deleted file mode 100644 index 7ba1a933..00000000 --- a/core/src/main/scala/dev/rudiments/codecs/BinaryDecoder.scala +++ /dev/null @@ -1,45 +0,0 @@ -package dev.rudiments.codecs - -import java.nio.ByteBuffer -import java.nio.charset.StandardCharsets.UTF_8 - -trait BinaryDecoder[A] { - extension (arr: Array[Byte]) def decode(): A -} - -object BinaryDecoder { - given intDecoder: BinaryDecoder[Int] with - extension (arr: Array[Byte]) - def decode(): Int = ByteBuffer.wrap(arr).getInt - - given longDecoder: BinaryDecoder[Long] with - extension (arr: Array[Byte]) - def decode(): Long = ByteBuffer.wrap(arr).getLong() - - given stringDecoder: BinaryDecoder[String] with - extension (arr: Array[Byte]) - def decode(): String = { - val buff = ByteBuffer.wrap(arr) - val size = buff.getInt - val a: Array[Byte] = new Array(size) - buff.get(a) - new String(a, UTF_8) - } - - given optionDecoder[T: BinaryDecoder]: BinaryDecoder[Option[T]] with - extension (arr: Array[Byte]) - def decode(): Option[T] = { - if (arr.isEmpty) None - else arr.decode() - } - - given iterableDecoder[T: BinaryDecoder]: BinaryDecoder[Iterable[T]] with - extension (arr: Array[Byte]) - def decode(): Iterable[T] = { - val buff = ByteBuffer.wrap(arr) - val size = buff.getInt - val a: Array[Byte] = new Array(size) - ??? - - } -} \ No newline at end of file diff --git a/core/src/main/scala/dev/rudiments/codecs/BinaryEncoder.scala b/core/src/main/scala/dev/rudiments/codecs/BinaryEncoder.scala deleted file mode 100644 index 9f9b11c9..00000000 --- a/core/src/main/scala/dev/rudiments/codecs/BinaryEncoder.scala +++ /dev/null @@ -1,57 +0,0 @@ -package dev.rudiments.codecs - -import java.nio.ByteBuffer -import java.nio.charset.StandardCharsets.UTF_8 - -trait BinaryEncoder[A] { - extension (a: A) def encode(): Array[Byte] -} - -object BinaryEncoder { - given intEncoder: BinaryEncoder[Int] with - extension (a: Int) - def encode(): Array[Byte] = ByteBuffer.allocate(4).putInt(a).array() - - given longEncoder: BinaryEncoder[Long] with - extension (a: Long) - def encode(): Array[Byte] = ByteBuffer.allocate(8).putLong(a).array() - - given stringEncoder: BinaryEncoder[String] with - extension (a: String) - def encode(): Array[Byte] = { - val arr = a.getBytes(UTF_8) - ByteBuffer.allocate(arr.size + 4) - .putInt(arr.size) - .put(arr) - .array() - } - - given optionEncoder[T: BinaryEncoder]: BinaryEncoder[Option[T]] with - extension (a: Option[T]) - def encode(): Array[Byte] = a match - case Some(v) => v.encode() - case None => Array.empty[Byte] - - given product2Encoder[A: BinaryEncoder, B: BinaryEncoder]: BinaryEncoder[Product2[A, B]] with - extension (p: Product2[A, B]) - def encode(): Array[Byte] = { - val a = p._1.encode() - val b = p._2.encode() - a ++ b - } - - //TODO study io.circe.EncoderDerivation & io.circe.Derivation#summonEncodersRec - given tuple2Encoder[A: BinaryEncoder, B: BinaryEncoder]: BinaryEncoder[(A, B)] with - extension (t: (A, B)) - def encode(): Array[Byte] = t._1.encode() ++ t._2.encode() - - given seqEncoder[T: BinaryEncoder]: BinaryEncoder[Seq[T]] with - extension (a: Seq[T]) - def encode(): Array[Byte] = { - val arr = a.foldLeft(Seq.empty[Byte]) { (acc, el) => acc ++ el.encode() } - ByteBuffer.allocate(arr.size + 4) - .putInt(arr.size) - .put(arr.toArray[Byte]) - .array() - } -} diff --git a/core/src/main/scala/dev/rudiments/codecs/BytesCodec.scala b/core/src/main/scala/dev/rudiments/codecs/BytesCodec.scala deleted file mode 100644 index ad2fd046..00000000 --- a/core/src/main/scala/dev/rudiments/codecs/BytesCodec.scala +++ /dev/null @@ -1,26 +0,0 @@ -package dev.rudiments.codecs - -import java.nio.ByteBuffer -import java.nio.charset.StandardCharsets.UTF_8 - -object BytesCodec { - def encodeStrings(strings: Seq[String]): Array[Byte] = { - val bytes = strings.map { s => - s.getBytes(UTF_8) - } - val size = 4 + bytes.size * 4 + bytes.map(_.length).sum // arr size + each element size + data - val buff = ByteBuffer.allocate(size).putInt(bytes.size) - bytes.foreach(b => buff.putInt(b.length).put(b)) - buff.array() - } - - def decodeString(bytes: Array[Byte]): Seq[String] = { - val buff = ByteBuffer.wrap(bytes) - (0 to buff.getInt).map { _ => - val size = buff.getInt - val arr = new Array[Byte](size) - buff.get(arr) - new String(arr, UTF_8) - } - } -} 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..107e9b1c --- /dev/null +++ b/core/src/main/scala/dev/rudiments/codecs/Codec.scala @@ -0,0 +1,41 @@ +package dev.rudiments.codecs + +import Coded.{Error, Ok} + +import scala.reflect.ClassTag + +class Encoder[A, B](en: A => Coded[B]) { + def map[C](f: B => C): Encoder[A, C] = Encoder(en.andThen(_.map(f))) +} +object Encoder { + def pure[A, B](f: A => B): Encoder[A, B] = Encoder(f.andThen(r => Ok(r))) +} + +class Decoder[A, B](de: A => Decoded[B]) { + def map[C](f: B => C): Decoder[A, C] = Decoder(de.andThen(_.map(f))) +} + +class Codec[A, B](en: A => Coded[B], de: B => Decoded[A]) { + def bimap[C]( + f: B => C, g: C => B + ): Codec[A, C] = Codec( + en.andThen(_.map(f)), g.andThen(de) + ) +} + +enum Coded[A] { + case Error(e: Exception) + case Ok(value: A) + + def map[B](f: A => B): Coded[B] = this match { + case Coded.Error(e) => Coded.Error(e) + case Coded.Ok(v) => Coded.Ok(f(v)) + } +} + +case class Encoded[A](value: Either[Exception, A]) { + def map[B](f: A => B): Encoded[B] = Encoded(value.map(f)) +} +case class Decoded[A](value: Either[Exception, A]) { + def map[B](f: A => B): Decoded[B] = Decoded(value.map(f)) +} 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 3c928b22..00000000 --- a/core/src/main/scala/dev/rudiments/hardcore/Memory.scala +++ /dev/null @@ -1,72 +0,0 @@ -package dev.rudiments.hardcore - -import scala.collection.mutable -import scala.reflect.ClassTag - -class Memory { // global context - //TODO nodes + schemas = structure - //TODO types - import Location.* - val values: mutable.Map[ID, Any] = mutable.Map.empty - - def read(key: Location): Readen | NotFound = { - key match { - case id: ID => values.get(id) match { - case Some(v) => Readen(v) - case None => NotFound(id) - } - case p: Path => NotFound(p) - } - } - - def create(key: Location, value: Any): Response = key match { - case id: ID => values.get(id) match { - case Some(v) => Conflict(Created(v), Readen(v)) - case None => values += id -> value; Created(value) - } - case p: Path => NotFound(p) - } - def update(key: Location, oldValue: Any, newValue: Any): Response = key match { - case id: ID => values.get(id) match { - case Some(v) if v == oldValue => values += id -> newValue; Updated(v, newValue) - case Some(v) => Conflict(Updated(v, newValue), Readen(v)) - case None => NotFound(id) - } - case p: Path => NotFound(p) - } - def delete(key: Location, oldValue: Any): Response = key match { - case id: ID => values.get(id) match { - case Some(v) if v == oldValue => values -= id; Deleted(v) - case Some(v) => Conflict(Deleted(oldValue), Readen(v)) - case None => NotFound(id) - } - case p: Path => NotFound(p) - } - - - def /(s: String): Reading = new Reading(this, ID(s)) - def /?(s: String): Response = this.read(ID(s)) - def -(s: String): Response = { - val k = ID(s) - this.read(k) match { - case nf@NotFound(_) => nf - case Readen(r) => this.delete(k, r) - } - } -} - -class Reading(mem: Memory, key: Location) { - def +(value: Any) : Response = mem.read(key) match { - case NotFound(_) => mem.create(key, value) - case r => Conflict(Created(value), r) - } - def *(value: Any): Response = mem.read(key) match { - case nf@NotFound(_) => nf - case Readen(r) => mem.update(key, r, value) - } -} - -enum Location { - case ID(s: String) - case Path(ids: Seq[String]) -} \ No newline at end of file diff --git a/core/src/main/scala/dev/rudiments/hardcore/Message.scala b/core/src/main/scala/dev/rudiments/hardcore/Message.scala deleted file mode 100644 index 1e9b0be7..00000000 --- a/core/src/main/scala/dev/rudiments/hardcore/Message.scala +++ /dev/null @@ -1,29 +0,0 @@ -package dev.rudiments.hardcore - -sealed trait Message extends Product -sealed trait In extends Message -sealed trait Out extends Message - -trait Command extends In -trait Query extends In -trait Event extends Out -trait Report extends Out -trait Error extends Report - -case class Create(value: Any) extends Command -case class Read(where: Location) extends Query -case class Update(old: Any, value: Any) extends Command -case class Delete(old: Any) extends Command - -case class Created(value: Any) extends Event -case class Readen(value: Any) extends Report -case class Updated(old: Any, value: Any) extends Event -case class Deleted(old: Any) extends Event - -case class NotFound(where: Location) extends Report -case object Identical extends Report -case class Conflict(in: Event, actual: Out) extends Error -case class NotSupported(m: Message) extends Error -case class InternalError(t: Throwable) extends Error - -type Response = Event | Report \ No newline at end of file diff --git a/core/src/main/scala/dev/rudiments/utils/SaltedMap.scala b/core/src/main/scala/dev/rudiments/utils/SaltedMap.scala deleted file mode 100644 index 2169e6ef..00000000 --- a/core/src/main/scala/dev/rudiments/utils/SaltedMap.scala +++ /dev/null @@ -1,56 +0,0 @@ -package dev.rudiments.utils - -class SaltedMap[K, +V](values: Array[(K, V)]) extends Map[K, V]{ - val salt: Int = 0;//TODO - - override def removed(key: K): SaltedMap[K, V] = new SaltedMap(values.filterNot(_._2 == key)) - - override def updated[V1 >: V](key: K, value: V1): SaltedMap[K, V1] = ???/*this.get(key) match { - case Some(v) if v == value => this - case Some(_) => - new SaltedMap[K, V1]( - values.update(saltedHash(key), key -> value.asInstanceOf[V]).asInstanceOf[Array[(K, V1)]] - ) //TODO improve, prob combine with get - case None => new SaltedMap(values :+ key -> value) - }*/ - - override def get(key: K): Option[V] = { - val found = values(saltedHash(key)) - if(found._1 == key) Some(found._2) else None - } - - override def iterator: Iterator[(K, V)] = values.iterator - - def saltedHash(key: K): Int = (key.hashCode() + salt) % values.length -} - -object SaltedMap extends Log { - def empty[K, V]: SaltedMap[K, V] = new SaltedMap[K, V](Array.empty) - - def apply[K, V](pairs: (K, V)*): SaltedMap[K, V] = new SaltedMap[K, V](pairs.toArray) - - - def h[K](key: K, size: Int, salt: Int): Int = { - val hash = 31 * 7 + salt - (31 * hash + key.hashCode()) % size - } - - val maxInterations: Int = Int.MaxValue - def salty[K](keys: List[K]): Int = { - var i: Int = 1 - var fit: Boolean = false - val expected = keys.indices - while(i <= maxInterations && !fit) { - fit = keys.map(k => h(k, keys.size, i)) == expected - if(!fit && i % 10_000_000 == 0) { - log.info("Failed on {}", i) - } - i += 1 - } - if(!fit) { - -1 - } else { - i - } - } -} \ No newline at end of file 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 a38639e9..00000000 --- a/core/src/test/scala/test/dev/rudiments/hardcore/MemorySpec.scala +++ /dev/null @@ -1,35 +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{ - case class Something(a: Int, b: String) - - "Memory" should { - import Location._ - given mem: Memory = new Memory - val sample = Something(42, "forty two") - val sample2 = Something(43, "forty three") - - "read and create in the location" in { - mem /? "42" should be (NotFound(ID("42"))) - mem / "42" + sample should be (Created(sample)) - mem /? "42" should be (Readen(sample)) - } - - "update in the location" in { - mem / "42" * sample2 should be (Updated(sample, sample2)) - mem /? "42" should be (Readen(sample2)) - } - - "delete in the location" in { - mem - "42" should be (Deleted(sample2)) - mem /? "42" should be (NotFound(ID("42"))) - } - } -} diff --git a/core/src/test/scala/test/dev/rudiments/utils/SaltedMapTest.scala b/core/src/test/scala/test/dev/rudiments/utils/SaltedMapTest.scala deleted file mode 100644 index 34a0dc11..00000000 --- a/core/src/test/scala/test/dev/rudiments/utils/SaltedMapTest.scala +++ /dev/null @@ -1,20 +0,0 @@ -package test.dev.rudiments.utils - -import dev.rudiments.utils.SaltedMap -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 SaltedMapTest extends AnyWordSpec with Matchers { - "Salted hash" should { - "fit for ordered keys" in { - SaltedMap.salty(0 :: 1 :: 2 :: 3 :: Nil) should be (4) - } - - "fit for unordered keys" ignore { - SaltedMap.salty(1 :: 3 :: 0 :: 2 :: Nil) should be(5) - } - } -} diff --git a/file/src/main/scala/dev/rudiments/file/About.scala b/file/src/main/scala/dev/rudiments/file/About.scala deleted file mode 100644 index d7370471..00000000 --- a/file/src/main/scala/dev/rudiments/file/About.scala +++ /dev/null @@ -1,43 +0,0 @@ -package dev.rudiments.file - -import dev.rudiments.utils.SHA3 - -import java.nio.ByteBuffer -import java.nio.charset.StandardCharsets.UTF_8 -import scala.collection.immutable.ArraySeq - - -case class About( - fileType: About.Type, - size: Int, - checksum: SHA3 -) { - def toByteArray: Array[Byte] = { - ByteBuffer.allocate(1 + 32 + 4) - .put(checksum.asArray) - .put((fileType.ordinal + 1).toByte) - .putInt(size) - .array() - } -} - -object About { - def apply(data: Array[Byte]): About = { - val buff = ByteBuffer.wrap(data) - val hash = new Array[Byte](32) - buff.get(hash) - val fileType = About.Type(buff.get()) - val size = buff.getInt - new About(fileType, size, new SHA3(ArraySeq.unsafeWrapArray(hash))) - } - - enum Type: - case File, Dir; - - object Type { - def apply(b: Byte): Type = b match - case 1 => File - case 2 => Dir - case _ => throw new IllegalArgumentException(s"Not supported type from header: $b") - } -} diff --git a/file/src/main/scala/dev/rudiments/file/Repository.scala b/file/src/main/scala/dev/rudiments/file/Repository.scala deleted file mode 100644 index e3697b54..00000000 --- a/file/src/main/scala/dev/rudiments/file/Repository.scala +++ /dev/null @@ -1,83 +0,0 @@ -package dev.rudiments.file - -import dev.rudiments.codecs.BytesCodec -import dev.rudiments.utils.SHA3 - -import java.io.File -import java.lang -import java.nio.ByteBuffer -import java.nio.file.{Path => FilePath} -import scala.collection.immutable.ArraySeq -import scala.collection.mutable - -class Repository(path: FilePath) { - private val dir = path.toAbsolutePath - - val state: mutable.Map[Path, FS] = mutable.Map.empty - val log: mutable.Buffer[Commit] = mutable.Buffer.empty - - val ignored: Set[Path] = Set(Seq(".git"), Seq(".gradle"), Seq(".idea")) - - def read(): Unit = { - given tx: Tx = new Tx(state.toMap) - val file = dir.toFile - val readen: FS = if(file.isFile) { - readFileUnsafe(file) - } else if(file.isDirectory) { - readDirUnsafe(file, Seq.empty) - } else { - throw new IllegalArgumentException("Not a file or dir") - } - tx.put(Seq.empty, readen) - - val commit = tx.makeCommit - if(commit.changes.nonEmpty) { - log.append(commit) - state ++= commit.changed - } - } - - private def readDirUnsafe(file: File, prefix: Path)(using tx: Tx): Dir = { - val content = file.listFiles().toList.sortBy(_.getName) - .filterNot(f => ignored.contains(prefix :+ f.getName)) - .map { - case f if f.isFile => f.getName -> readFileUnsafe(f) - case f if f.isDirectory => f.getName -> readDirUnsafe(f, prefix :+ f.getName) - } - - content.foreach((k, v) => tx.put(prefix :+ k, v)) - - val (dirs, blobs) = content.partitionMap { - case (s, _: Dir) => Left(s) - case (s, _: Binary) => Right(s) - } - Dir(dirs, blobs) - } - - private def readFileUnsafe(file: File): Binary = { - Binary(java.nio.file.Files.readAllBytes(file.toPath)) - } -} - -type Path = Seq[String] - -sealed trait FS { - def about: About -} - -import dev.rudiments.file.About.Type -case class Dir(directions: Path, files: Path) extends FS { - lazy val data: Array[Byte] = BytesCodec.encodeStrings(directions) ++ BytesCodec.encodeStrings(files) - - override def about: About = About(Type.Dir, data.length, SHA3(data)) -} -case class Binary(data: Seq[Byte]) extends FS { - override def about: About = About(Type.File, data.size, SHA3(data.toArray[Byte])) -} -object Binary { - def apply(data: Array[Byte]): Binary = new Binary(ArraySeq.unsafeWrapArray(data)) -} - -case object NotExist extends FS { - override def about: About = About(Type.File, 0, SHA3.empty) -} \ No newline at end of file diff --git a/file/src/main/scala/dev/rudiments/file/Tx.scala b/file/src/main/scala/dev/rudiments/file/Tx.scala deleted file mode 100644 index 569a54d5..00000000 --- a/file/src/main/scala/dev/rudiments/file/Tx.scala +++ /dev/null @@ -1,69 +0,0 @@ -package dev.rudiments.file - -import dev.rudiments.utils.SHA3 - -import scala.collection.mutable - -class Tx(val initial: Map[Path, FS]) { - val changed: mutable.Map[Path, FS] = mutable.Map.empty - val log: mutable.Map[Path, FileLog] = mutable.Map.empty - - def put(k: Path, v: FS): Unit = { - initial.get(k) match - case Some(found) => - if (found.about != v.about) { - changed.put(k, v) - log.put(k, FileLog(Some(found.about.checksum), Some(v.about.checksum))) - - (found, v) match { - case (Dir(d1, f1), Dir(d2, f2)) => - (d1.toSet -- d2.toSet).foreach { s => deleting(k :+ s) } - (f1.toSet -- f2.toSet).foreach { s => deleting(k :+ s) } - case (Dir(d, f), _) => - d.foreach { s => deleting(k :+ s) } - f.foreach { s => deleting(k :+ s) } - case _ => //do nothing - } - } else { - changed.remove(k) - log.remove(k) - } - case None => - v match { - case NotExist => - changed.remove(k) - log.remove(k) - case _ => - changed.put(k, v) - log.put(k, FileLog(None, Some(v.about.checksum))) - } - } - - def makeCommit: Commit = Commit(changed.toMap.map((k,change) => k -> (log(k), change))) - - def deleting(from: Path): Unit = { //TODO replace with Delete on Dir instead of Repo - initial.get(from) match { - case Some(d@Dir(files, dirs)) => - changed.put(from, NotExist) - log.put(from, FileLog(None, Some(d.about.checksum))) - dirs.foreach(s => deleting(from :+ s)) - files.foreach(s => deleting(from :+ s)) - case Some(b: Binary) => - changed.put(from, NotExist) - log.put(from, FileLog(None, Some(b.about.checksum))) - case _ => //DO nothing - } - } -} - - -case class FileLog( - before: Option[SHA3], - after: Option[SHA3] -) - -case class Commit( - changes: Map[Path, (FileLog, FS)] -) { - def changed: Map[Path, FS] = changes.map { case (k, (_, change)) => k -> change } -} \ No newline at end of file diff --git a/file/src/test/scala/test/dev/rudiments/file/FileTest.scala b/file/src/test/scala/test/dev/rudiments/file/FileTest.scala deleted file mode 100644 index efa3f9bb..00000000 --- a/file/src/test/scala/test/dev/rudiments/file/FileTest.scala +++ /dev/null @@ -1,42 +0,0 @@ -package test.dev.rudiments.file - -import dev.rudiments.file.* -import dev.rudiments.utils.SHA3 -import org.junit.runner.RunWith -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec -import org.scalatestplus.junit.JUnitRunner - -import java.nio.charset.StandardCharsets.UTF_8 -import java.nio.file.{Path => FilePath} - -@RunWith(classOf[JUnitRunner]) -class FileTest extends AnyWordSpec with Matchers { - private val dir = FilePath.of("..", "file", "src", "test", "resources", "example").toAbsolutePath - val repo = new Repository(dir) - - "can read repository" in { - repo.read() - repo.state.size should be (4) - repo.state.toMap should be ( - Map[Path, FS]( - Seq.empty -> Dir(Seq("nested"), Seq("1.txt")), - Seq("nested") -> Dir(Seq.empty, Seq("2.txt")), - Seq("nested", "2.txt") -> Binary("second file".getBytes(UTF_8)), - Seq("1.txt") -> Binary("first file".getBytes(UTF_8)) - ) - ) - } - - "can read hole project" ignore { - val r = new Repository(FilePath.of(".")) - r.read() - r.log.size should be (1) - } - - "can read git project" ignore { - val r = new Repository(FilePath.of("../git")) - r.read() - r.log.size should be(1) - } -} From 8873d21a5b82807348b8d5b769048595c7dbc940 Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 26 Oct 2023 18:00:19 +0400 Subject: [PATCH 51/75] refactor --- .../scala/dev/rudiments/codecs/Codec.scala | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/core/src/main/scala/dev/rudiments/codecs/Codec.scala b/core/src/main/scala/dev/rudiments/codecs/Codec.scala index 107e9b1c..32f2977c 100644 --- a/core/src/main/scala/dev/rudiments/codecs/Codec.scala +++ b/core/src/main/scala/dev/rudiments/codecs/Codec.scala @@ -1,21 +1,20 @@ package dev.rudiments.codecs -import Coded.{Error, Ok} - import scala.reflect.ClassTag -class Encoder[A, B](en: A => Coded[B]) { - def map[C](f: B => C): Encoder[A, C] = Encoder(en.andThen(_.map(f))) +class Encoder[A, B](en: A => Result[B]) extends OneWay(en) { + def toCodec(de: B => Result[A]): Codec[A, B] = Codec(en, de) } object Encoder { - def pure[A, B](f: A => B): Encoder[A, B] = Encoder(f.andThen(r => Ok(r))) + def pure[A, B](f: A => B): Encoder[A, B] = Encoder(f.andThen(r => Result.Ok(r))) + //TODO if error in B } -class Decoder[A, B](de: A => Decoded[B]) { - def map[C](f: B => C): Decoder[A, C] = Decoder(de.andThen(_.map(f))) +class Decoder[A, B](de: A => Result[B]) extends OneWay(de){ + def toCodec(en: B => Result[A]): Codec[B, A] = Codec(en, de) } -class Codec[A, B](en: A => Coded[B], de: B => Decoded[A]) { +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( @@ -23,19 +22,16 @@ class Codec[A, B](en: A => Coded[B], de: B => Decoded[A]) { ) } -enum Coded[A] { +enum Result[A] { case Error(e: Exception) case Ok(value: A) - def map[B](f: A => B): Coded[B] = this match { - case Coded.Error(e) => Coded.Error(e) - case Coded.Ok(v) => Coded.Ok(f(v)) + 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)) } } -case class Encoded[A](value: Either[Exception, A]) { - def map[B](f: A => B): Encoded[B] = Encoded(value.map(f)) -} -case class Decoded[A](value: Either[Exception, A]) { - def map[B](f: A => B): Decoded[B] = Decoded(value.map(f)) +class OneWay[A, B](t: A => Result[B]) { + def map[C](f: B => C): OneWay[A, C] = OneWay(t.andThen(_.map(f))) } From 15b5703945af9bde8592802f54940bbca12c90ff Mon Sep 17 00:00:00 2001 From: gennady Date: Mon, 13 Nov 2023 10:48:00 +0400 Subject: [PATCH 52/75] draft Graph --- .../scala/dev/rudiments/codecs/Codec.scala | 15 +++-- .../scala/dev/rudiments/hardcore/Graph.scala | 65 +++++++++++++++++++ .../dev/rudiments/hardcore/GraphTest.scala | 15 +++++ 3 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 core/src/main/scala/dev/rudiments/hardcore/Graph.scala create mode 100644 core/src/test/scala/test/dev/rudiments/hardcore/GraphTest.scala diff --git a/core/src/main/scala/dev/rudiments/codecs/Codec.scala b/core/src/main/scala/dev/rudiments/codecs/Codec.scala index 32f2977c..946c249b 100644 --- a/core/src/main/scala/dev/rudiments/codecs/Codec.scala +++ b/core/src/main/scala/dev/rudiments/codecs/Codec.scala @@ -2,16 +2,18 @@ package dev.rudiments.codecs import scala.reflect.ClassTag -class Encoder[A, B](en: A => Result[B]) extends OneWay(en) { +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 pure[A, B](f: A => B): Encoder[A, B] = Encoder(f.andThen(r => Result.Ok(r))) + 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](de: A => Result[B]) extends OneWay(de){ - def toCodec(en: B => Result[A]): Codec[B, A] = Codec(en, de) +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]) { @@ -30,6 +32,11 @@ enum Result[A] { 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](t: A => Result[B]) { 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..b751bb7c --- /dev/null +++ b/core/src/main/scala/dev/rudiments/hardcore/Graph.scala @@ -0,0 +1,65 @@ +package dev.rudiments.hardcore + +import dev.rudiments.codecs.{Codec, Decoder, Encoder, Result} +import dev.rudiments.hardcore.Graph.{AroundNode, Edge, Edges, Item, SeqGraph} + +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 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 + Graph( + newNodes.map(a => a.key -> a.node).toMap, + newNodes.flatMap(a => a.from) //TODO what to do if in != out edges? + ) + } + + def stitch(sub: Graph[K, N, E], stitches: Edges[K, E] = Seq.empty): Graph[K, N, E] = ??? + def cut(sub: Graph[K, N, E]): (Graph[K, N, E], Edges[K, E]) = ??? + + def to[A](using en: Encoder[Graph[K, N, E], A]): Result[A] = en.en(this) + /* TODO: + aggregate + */ +} + +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 + ) + } +} 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..6ad50794 --- /dev/null +++ b/core/src/test/scala/test/dev/rudiments/hardcore/GraphTest.scala @@ -0,0 +1,15 @@ +package test.dev.rudiments.hardcore + +import dev.rudiments.hardcore.Graph +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 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)) + } +} From 122f43e25eb8ab937d53c19e1261afc36b77d534 Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 23 Nov 2023 11:15:49 +0400 Subject: [PATCH 53/75] implement some methods --- .../scala/dev/rudiments/hardcore/Graph.scala | 69 ++++++++++++++----- 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/core/src/main/scala/dev/rudiments/hardcore/Graph.scala b/core/src/main/scala/dev/rudiments/hardcore/Graph.scala index b751bb7c..0b61c22a 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Graph.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Graph.scala @@ -3,46 +3,79 @@ package dev.rudiments.hardcore import dev.rudiments.codecs.{Codec, Decoder, Encoder, Result} import dev.rudiments.hardcore.Graph.{AroundNode, Edge, Edges, Item, SeqGraph} -case class Graph[K, N, E]( +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 + .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, - newNodes.flatMap(a => a.from) //TODO what to do if in != out edges? + 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 stitch(sub: Graph[K, N, E], stitches: Edges[K, E] = Seq.empty): Graph[K, N, E] = ??? - def cut(sub: Graph[K, N, E]): (Graph[K, N, E], Edges[K, E]) = ??? + 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] = Seq.empty): Graph[K, N1, E1] = Graph( + nodes = this.nodes ++ that.nodes, + edges = this.edges ++ that.edges ++ joints + ) + def split(keys: Set[K]): (Graph[K, N, E], Edges[K, E], Graph[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) + } + (this.filter { (k, _) => keys.contains(k) }, joints, this.filter { (k, _) => cutNodeKeys.contains(k) }) + } - def to[A](using en: Encoder[Graph[K, N, E], A]): Result[A] = en.en(this) - /* TODO: - aggregate - */ + 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]] + 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]]) + 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]]) From 7e971a3c10728f4f953c77a31880e54034436a79 Mon Sep 17 00:00:00 2001 From: gennady Date: Sun, 17 Dec 2023 10:58:45 +0400 Subject: [PATCH 54/75] some types --- .../dev/rudiments/hardcore/EdgeTree.scala | 31 ++++++++++ .../scala/dev/rudiments/hardcore/Graph.scala | 18 ++++-- .../dev/rudiments/hardcore/Location.scala | 5 ++ .../dev/rudiments/hardcore/TypeSystem.scala | 62 +++++++++++++++++++ .../test/dev/rudiments/codecs/CodecTest.scala | 15 +++++ 5 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 core/src/main/scala/dev/rudiments/hardcore/EdgeTree.scala create mode 100644 core/src/main/scala/dev/rudiments/hardcore/Location.scala create mode 100644 core/src/main/scala/dev/rudiments/hardcore/TypeSystem.scala create mode 100644 core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala diff --git a/core/src/main/scala/dev/rudiments/hardcore/EdgeTree.scala b/core/src/main/scala/dev/rudiments/hardcore/EdgeTree.scala new file mode 100644 index 00000000..e81246f1 --- /dev/null +++ b/core/src/main/scala/dev/rudiments/hardcore/EdgeTree.scala @@ -0,0 +1,31 @@ +package dev.rudiments.hardcore + +import dev.rudiments.hardcore.Graph.Edges + +case class EdgeTree[K, +B, +L, +E]( + leafs: Map[List[K], L], + branches: Map[List[K], B], + edges: Edges[List[K], E] +) { + lazy val edgesFrom: Map[List[K], Edges[List[K], E]] = edges.groupBy(_.from) + lazy val edgesTo: Map[List[K], Edges[List[K], E]] = edges.groupBy(_.to) + + if (leafs.keySet.intersect(branches.keySet).nonEmpty) { + throw new IllegalArgumentException(s"Branches and leafs have intersecting keys: ${leafs.keySet.intersect(branches.keySet)}") + } + if (((edgesFrom.keySet ++ edgesTo.keySet) -- (leafs.keySet ++ branches.keySet)).nonEmpty) { + throw new IllegalArgumentException(s"Edges have external keys") + } + + def toGraph[E2 >: E](defaultEdge: B => E2): Graph[List[K], B | L, E] = { + val defaultEdges = branches.map { case (k, v) => Graph.Edge(k, k, defaultEdge(v).asInstanceOf[E]) }.toSeq + Graph( + branches ++ leafs, + defaultEdges ++ edges + ) + } +} + +object EdgeTree { + +} \ No newline at end of file diff --git a/core/src/main/scala/dev/rudiments/hardcore/Graph.scala b/core/src/main/scala/dev/rudiments/hardcore/Graph.scala index 0b61c22a..b8a0ab25 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Graph.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Graph.scala @@ -1,7 +1,7 @@ package dev.rudiments.hardcore -import dev.rudiments.codecs.{Codec, Decoder, Encoder, Result} -import dev.rudiments.hardcore.Graph.{AroundNode, Edge, Edges, Item, SeqGraph} +import dev.rudiments.codecs.{Encoder, Result} +import dev.rudiments.hardcore.Graph.{AroundNode, Edge, Edges, Item, JointGraph, SeqGraph} case class Graph[K, +N, +E]( nodes: Map[K, N], @@ -55,16 +55,20 @@ case class Graph[K, +N, +E]( 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] = Seq.empty): Graph[K, N1, E1] = Graph( + 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]): (Graph[K, N, E], Edges[K, E], Graph[K, N, E]) = { + 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) } - (this.filter { (k, _) => keys.contains(k) }, joints, this.filter { (k, _) => cutNodeKeys.contains(k) }) + 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) @@ -95,4 +99,8 @@ object Graph { }, 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/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/TypeSystem.scala b/core/src/main/scala/dev/rudiments/hardcore/TypeSystem.scala new file mode 100644 index 00000000..399c0e50 --- /dev/null +++ b/core/src/main/scala/dev/rudiments/hardcore/TypeSystem.scala @@ -0,0 +1,62 @@ +package dev.rudiments.hardcore + +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 + } + + def nonAbstract: Boolean = !isAbstract + + def make(data: Any): Data = validate(data) match + case Right(v) => Data(this, v) + case Left(err) => throw err +} + +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) +} + +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/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..49febca4 --- /dev/null +++ b/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala @@ -0,0 +1,15 @@ +package test.dev.rudiments.codecs + +import dev.rudiments.hardcore.{EdgeTree, Graph} +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 CodecTest extends AnyWordSpec with Matchers { + + "can generate Codecs graph from the Type graph" in { + //TODO Type Graph with custom types + } +} From a4b223a87f55fa192853a06f0cc9c6a8ec619c20 Mon Sep 17 00:00:00 2001 From: gennady Date: Sun, 17 Dec 2023 18:56:53 +0400 Subject: [PATCH 55/75] prepare for auto-derivation --- .../scala/dev/rudiments/codecs/Codec.scala | 2 +- .../main/scala/dev/rudiments/codecs/MJ.scala | 30 ++++++++++++++++ .../test/dev/rudiments/codecs/CodecTest.scala | 35 ++++++++++++++++++- 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 core/src/main/scala/dev/rudiments/codecs/MJ.scala diff --git a/core/src/main/scala/dev/rudiments/codecs/Codec.scala b/core/src/main/scala/dev/rudiments/codecs/Codec.scala index 946c249b..658bd899 100644 --- a/core/src/main/scala/dev/rudiments/codecs/Codec.scala +++ b/core/src/main/scala/dev/rudiments/codecs/Codec.scala @@ -39,6 +39,6 @@ enum Result[A] { } } -class OneWay[A, B](t: A => Result[B]) { +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..93b30625 --- /dev/null +++ b/core/src/main/scala/dev/rudiments/codecs/MJ.scala @@ -0,0 +1,30 @@ +package dev.rudiments.codecs + +object MJ { + 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)) + } + ) +} + +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/test/scala/test/dev/rudiments/codecs/CodecTest.scala b/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala index 49febca4..03488e4b 100644 --- a/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala +++ b/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala @@ -1,6 +1,10 @@ package test.dev.rudiments.codecs -import dev.rudiments.hardcore.{EdgeTree, Graph} +import dev.rudiments.codecs.TS +import dev.rudiments.codecs.MJ +import dev.rudiments.codecs.OneWay +import dev.rudiments.codecs.Result.* +import dev.rudiments.hardcore.{EdgeTree, Graph, Many} import org.junit.runner.RunWith import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -12,4 +16,33 @@ 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), + )))) + } } From 9242750df8e1d792f37df738562f0aa54a0fbca1 Mon Sep 17 00:00:00 2001 From: gennady Date: Mon, 18 Dec 2023 22:40:44 +0400 Subject: [PATCH 56/75] use some reflection and auto-derivation --- .../main/scala/dev/rudiments/codecs/MJ.scala | 35 +++++++++++++++++- .../dev/rudiments/codecs/MirrorInfo.scala | 37 +++++++++++++++++++ .../test/dev/rudiments/codecs/CodecTest.scala | 20 ++++++++-- .../test/dev/rudiments/codecs/Sample.scala | 5 +++ 4 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 core/src/main/scala/dev/rudiments/codecs/MirrorInfo.scala create mode 100644 core/src/test/scala/test/dev/rudiments/codecs/Sample.scala diff --git a/core/src/main/scala/dev/rudiments/codecs/MJ.scala b/core/src/main/scala/dev/rudiments/codecs/MJ.scala index 93b30625..5b13ea2a 100644 --- a/core/src/main/scala/dev/rudiments/codecs/MJ.scala +++ b/core/src/main/scala/dev/rudiments/codecs/MJ.scala @@ -1,6 +1,13 @@ package dev.rudiments.codecs -object MJ { +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 => @@ -20,6 +27,32 @@ object MJ { } 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 inline 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 { 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..8a4fb8f9 --- /dev/null +++ b/core/src/main/scala/dev/rudiments/codecs/MirrorInfo.scala @@ -0,0 +1,37 @@ +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) + + 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 inline 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/test/scala/test/dev/rudiments/codecs/CodecTest.scala b/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala index 03488e4b..527ca9b0 100644 --- a/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala +++ b/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala @@ -1,8 +1,6 @@ package test.dev.rudiments.codecs -import dev.rudiments.codecs.TS -import dev.rudiments.codecs.MJ -import dev.rudiments.codecs.OneWay +import dev.rudiments.codecs.{MJ, MirrorInfo, OneWay, TS} import dev.rudiments.codecs.Result.* import dev.rudiments.hardcore.{EdgeTree, Graph, Many} import org.junit.runner.RunWith @@ -10,6 +8,9 @@ import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec import org.scalatestplus.junit.JUnitRunner +import scala.compiletime.{constValue, erasedValue, error, summonFrom} +import scala.deriving.Mirror + @RunWith(classOf[JUnitRunner]) class CodecTest extends AnyWordSpec with Matchers { @@ -45,4 +46,17 @@ class CodecTest extends AnyWordSpec with Matchers { TS.Text("24") -> TS.Number(24), )))) } + + "can derive int and string fields of a case class and recursively" in { + MirrorInfo[Sample] should be ( + MirrorInfo[Sample]("Sample", Seq("i" -> MirrorInfo.intInfo, "s" -> MirrorInfo.strInfo)) + ) + + MirrorInfo[Example] should be( + MirrorInfo[Example]("Example", Seq( + "i" -> MirrorInfo.intInfo, + "s" -> MirrorInfo[Sample]("Sample", Seq("i" -> MirrorInfo.intInfo, "s" -> MirrorInfo.strInfo)) + )) + ) + } } diff --git a/core/src/test/scala/test/dev/rudiments/codecs/Sample.scala b/core/src/test/scala/test/dev/rudiments/codecs/Sample.scala new file mode 100644 index 00000000..75ad1904 --- /dev/null +++ b/core/src/test/scala/test/dev/rudiments/codecs/Sample.scala @@ -0,0 +1,5 @@ +package test.dev.rudiments.codecs + +case class Sample(i: Int, s: String) + +case class Example(i: Int, s: Sample) \ No newline at end of file From 8834b05eca5b02bc5cb292ad7c6a58148d70a1e1 Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 25 Jan 2024 11:24:57 +0400 Subject: [PATCH 57/75] update gradle configs and dependencies versions --- build.gradle | 55 ------------------ buildSrc/build.gradle | 7 +++ buildSrc/settings.gradle | 7 +++ ...dev.rudiments.scala-app-conventions.gradle | 4 ++ .../dev.rudiments.scala-conventions.gradle | 35 +++++++++++ ...dev.rudiments.scala-lib-conventions.gradle | 4 ++ core/build.gradle | 4 ++ example/build.gradle | 10 ++-- file/build.gradle | 4 ++ git/build.gradle | 4 ++ gradle/libs.versions.toml | 2 + gradle/wrapper/gradle-wrapper.jar | Bin 62076 -> 43462 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- settings.gradle | 9 +-- 14 files changed, 84 insertions(+), 64 deletions(-) delete mode 100644 build.gradle create mode 100644 buildSrc/build.gradle create mode 100644 buildSrc/settings.gradle create mode 100644 buildSrc/src/main/groovy/dev.rudiments.scala-app-conventions.gradle create mode 100644 buildSrc/src/main/groovy/dev.rudiments.scala-conventions.gradle create mode 100644 buildSrc/src/main/groovy/dev.rudiments.scala-lib-conventions.gradle create mode 100644 gradle/libs.versions.toml diff --git a/build.gradle b/build.gradle deleted file mode 100644 index b05bc92c..00000000 --- a/build.gradle +++ /dev/null @@ -1,55 +0,0 @@ -buildscript { - repositories { - maven { - url "https://plugins.gradle.org/m2/" - } - } -} - -def base_version = '0.5-SNAPSHOT' - -allprojects { - group 'dev.rudiments' - version base_version - - repositories { - mavenLocal() - mavenCentral() - } -} - -def scalaModules() { - subprojects.findAll { new File(it.projectDir, 'src/main/scala').directory } -} - -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" - } - } - - dependencies { - implementation 'org.scala-lang:scala3-library_3:3.1.3' - - implementation 'org.slf4j:slf4j-api:2.0.6' - - testImplementation 'org.scalatest:scalatest_3:3.2.15' - testImplementation 'org.scalatestplus:junit-4-13_3:3.2.15.0' - - testImplementation 'junit:junit:4.13.2' - testImplementation 'ch.qos.logback:logback-classic:1.4.5' - } -} \ 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..74ae1e10 --- /dev/null +++ b/buildSrc/src/main/groovy/dev.rudiments.scala-conventions.gradle @@ -0,0 +1,35 @@ +plugins { + id 'scala' +} + +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.junit.jupiter:junit-jupiter:5.10.1' + testImplementation 'org.scalatest:scalatest_3:3.2.17' + testImplementation 'org.scalatestplus:junit-5-10_3:3.2.17.0' + + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testRuntimeOnly 'ch.qos.logback:logback-classic:1.4.14' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +tasks.named('test') { + useJUnitPlatform() +} 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 dc540ace..6c83c2ce 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -1,3 +1,7 @@ +plugins { + id 'dev.rudiments.scala-lib-conventions' +} + dependencies { implementation 'io.github.java-diff-utils:java-diff-utils:4.12' diff --git a/example/build.gradle b/example/build.gradle index 4bd26c20..4c800ab2 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -1,13 +1,15 @@ +plugins { + id 'dev.rudiments.scala-app-conventions' +} + dependencies { implementation project(':core') implementation project(':file') implementation project(':git') - implementation 'ch.qos.logback:logback-classic:1.4.5' + implementation 'ch.qos.logback:logback-classic' } -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/file/build.gradle b/file/build.gradle index e4dbb7fe..c5c6a61e 100644 --- a/file/build.gradle +++ b/file/build.gradle @@ -1,3 +1,7 @@ +plugins { + id 'dev.rudiments.scala-lib-conventions' +} + dependencies { implementation project(':core') } diff --git a/git/build.gradle b/git/build.gradle index ce428f3b..8b79d288 100644 --- a/git/build.gradle +++ b/git/build.gradle @@ -1,3 +1,7 @@ +plugins { + id 'dev.rudiments.scala-lib-conventions' +} + dependencies { implementation project(':core') implementation project(':file') 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 c1962a79e29d3e0ab67b14947c167a862655af9b..d64cd4917707c1f8861d8cb53dd15194d4248596 100644 GIT binary patch literal 43462 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 literal 62076 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0c85a1f7..1af9e093 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-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/settings.gradle b/settings.gradle index 0c00cb08..ad008c53 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,6 @@ +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0' +} + rootProject.name = 'hardcore' -include 'core' -include 'file' -include 'git' -include 'example' +include('core', 'file', 'git', 'example') \ No newline at end of file From fc5c2dec2fc589eb254d79402e7ba80845b459fb Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 25 Jan 2024 12:06:20 +0400 Subject: [PATCH 58/75] fit tests for junit5 runner downgrade scala version - see https://github.com/lampepfl/dotty/issues/16554 --- .../dev.rudiments.scala-conventions.gradle | 20 +++++++++++++------ .../test/dev/rudiments/codecs/CirceTest.scala | 3 --- .../test/dev/rudiments/codecs/CodecTest.scala | 3 --- .../dev/rudiments/hardcore/GraphTest.scala | 3 --- .../test/dev/rudiments/utils/DiffTest.scala | 3 --- .../test/dev/rudiments/utils/HashedTest.scala | 3 --- example/build.gradle | 2 +- .../test/dev/rudiments/app/CheckTest.scala | 3 --- .../test/dev/rudiments/git/GitBlobTest.scala | 3 --- .../dev/rudiments/git/GitCommitsTest.scala | 3 --- .../dev/rudiments/git/GitObjectTest.scala | 3 --- .../test/dev/rudiments/git/PackTest.scala | 3 --- .../dev/rudiments/git/RepositoryTest.scala | 3 --- 13 files changed, 15 insertions(+), 40 deletions(-) diff --git a/buildSrc/src/main/groovy/dev.rudiments.scala-conventions.gradle b/buildSrc/src/main/groovy/dev.rudiments.scala-conventions.gradle index 74ae1e10..a5133bf2 100644 --- a/buildSrc/src/main/groovy/dev.rudiments.scala-conventions.gradle +++ b/buildSrc/src/main/groovy/dev.rudiments.scala-conventions.gradle @@ -13,23 +13,31 @@ group = 'dev.rudiments' version = baseVersion dependencies { - implementation 'org.scala-lang:scala3-library_3:3.3.1' + implementation 'org.scala-lang:scala3-library_3:3.2.2' implementation 'org.slf4j:slf4j-api:2.0.11' - testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' + testImplementation 'org.scalatest:scalatest_3:3.2.17' testImplementation 'org.scalatestplus:junit-5-10_3:3.2.17.0' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + 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) + languageVersion = JavaLanguageVersion.of(17) } } -tasks.named('test') { - useJUnitPlatform() +test { + useJUnitPlatform { + includeEngines 'scalatest' + testLogging { + events("passed", "skipped", "failed", "standard_error") + } + } } diff --git a/core/src/test/scala/test/dev/rudiments/codecs/CirceTest.scala b/core/src/test/scala/test/dev/rudiments/codecs/CirceTest.scala index b84974c1..bec35b9f 100644 --- a/core/src/test/scala/test/dev/rudiments/codecs/CirceTest.scala +++ b/core/src/test/scala/test/dev/rudiments/codecs/CirceTest.scala @@ -1,14 +1,11 @@ package test.dev.rudiments.codecs -import org.junit.runner.RunWith import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec -import org.scalatestplus.junit.JUnitRunner import io.circe.generic.semiauto.* import io.circe.{Codec, Json} -@RunWith(classOf[JUnitRunner]) class CirceTest extends AnyWordSpec with Matchers { case class Sample( a: Int, diff --git a/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala b/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala index 527ca9b0..86c13c91 100644 --- a/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala +++ b/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala @@ -3,15 +3,12 @@ package test.dev.rudiments.codecs import dev.rudiments.codecs.{MJ, MirrorInfo, OneWay, TS} import dev.rudiments.codecs.Result.* import dev.rudiments.hardcore.{EdgeTree, Graph, Many} -import org.junit.runner.RunWith import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec -import org.scalatestplus.junit.JUnitRunner import scala.compiletime.{constValue, erasedValue, error, summonFrom} import scala.deriving.Mirror -@RunWith(classOf[JUnitRunner]) class CodecTest extends AnyWordSpec with Matchers { "can generate Codecs graph from the Type graph" in { diff --git a/core/src/test/scala/test/dev/rudiments/hardcore/GraphTest.scala b/core/src/test/scala/test/dev/rudiments/hardcore/GraphTest.scala index 6ad50794..21abba7f 100644 --- a/core/src/test/scala/test/dev/rudiments/hardcore/GraphTest.scala +++ b/core/src/test/scala/test/dev/rudiments/hardcore/GraphTest.scala @@ -1,12 +1,9 @@ package test.dev.rudiments.hardcore import dev.rudiments.hardcore.Graph -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 GraphTest extends AnyWordSpec with Matchers { "can create empty graph" in { diff --git a/core/src/test/scala/test/dev/rudiments/utils/DiffTest.scala b/core/src/test/scala/test/dev/rudiments/utils/DiffTest.scala index 2e982c31..5ae558d5 100644 --- a/core/src/test/scala/test/dev/rudiments/utils/DiffTest.scala +++ b/core/src/test/scala/test/dev/rudiments/utils/DiffTest.scala @@ -2,14 +2,11 @@ package test.dev.rudiments.utils import com.github.difflib.DiffUtils import dev.rudiments.utils.{Chunk, Delta, Diff, Unified} -import org.junit.runner.RunWith import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec -import org.scalatestplus.junit.JUnitRunner import scala.jdk.CollectionConverters.* -@RunWith(classOf[JUnitRunner]) 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 --git a/core/src/test/scala/test/dev/rudiments/utils/HashedTest.scala b/core/src/test/scala/test/dev/rudiments/utils/HashedTest.scala index 6bec0f7d..378d16f3 100644 --- a/core/src/test/scala/test/dev/rudiments/utils/HashedTest.scala +++ b/core/src/test/scala/test/dev/rudiments/utils/HashedTest.scala @@ -1,12 +1,9 @@ package test.dev.rudiments.utils import dev.rudiments.utils.{SHA256, SHA3, SHA1} -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 HashedTest extends AnyWordSpec with Matchers { "SHA-1 hash" should { "fit with known hashes" in { diff --git a/example/build.gradle b/example/build.gradle index 4c800ab2..748412e3 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -7,7 +7,7 @@ dependencies { implementation project(':file') implementation project(':git') - implementation 'ch.qos.logback:logback-classic' + implementation 'ch.qos.logback:logback-classic:1.4.14' } application { diff --git a/example/src/test/scala/test/dev/rudiments/app/CheckTest.scala b/example/src/test/scala/test/dev/rudiments/app/CheckTest.scala index 68ad425a..907565e3 100644 --- a/example/src/test/scala/test/dev/rudiments/app/CheckTest.scala +++ b/example/src/test/scala/test/dev/rudiments/app/CheckTest.scala @@ -1,11 +1,8 @@ package test.dev.rudiments.app -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 CheckTest extends AnyWordSpec with Matchers { "always true" in { val a = true diff --git a/git/src/test/scala/test/dev/rudiments/git/GitBlobTest.scala b/git/src/test/scala/test/dev/rudiments/git/GitBlobTest.scala index 952c943a..fc8be92c 100644 --- a/git/src/test/scala/test/dev/rudiments/git/GitBlobTest.scala +++ b/git/src/test/scala/test/dev/rudiments/git/GitBlobTest.scala @@ -2,12 +2,9 @@ package test.dev.rudiments.git import dev.rudiments.git.Blob import dev.rudiments.utils.SHA1 -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 GitBlobTest extends AnyWordSpec with Matchers { "Git BLOB" should { "fit header with example" in { diff --git a/git/src/test/scala/test/dev/rudiments/git/GitCommitsTest.scala b/git/src/test/scala/test/dev/rudiments/git/GitCommitsTest.scala index a05232a8..99a8dea4 100644 --- a/git/src/test/scala/test/dev/rudiments/git/GitCommitsTest.scala +++ b/git/src/test/scala/test/dev/rudiments/git/GitCommitsTest.scala @@ -2,14 +2,11 @@ package test.dev.rudiments.git import dev.rudiments.git.{Commit, Reader} import dev.rudiments.utils.Log -import org.junit.runner.RunWith import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec -import org.scalatestplus.junit.JUnitRunner import java.nio.file.{Files, Path} -@RunWith(classOf[JUnitRunner]) class GitCommitsTest extends AnyWordSpec with Matchers with Log { private val dir = Path.of("..").toAbsolutePath //TODO fix diff --git a/git/src/test/scala/test/dev/rudiments/git/GitObjectTest.scala b/git/src/test/scala/test/dev/rudiments/git/GitObjectTest.scala index d3455e2b..24e71a81 100644 --- a/git/src/test/scala/test/dev/rudiments/git/GitObjectTest.scala +++ b/git/src/test/scala/test/dev/rudiments/git/GitObjectTest.scala @@ -2,14 +2,11 @@ package test.dev.rudiments.git import dev.rudiments.git.{Blob, Commit, Reader, Tree, Writer} import dev.rudiments.utils.Log -import org.junit.runner.RunWith import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec -import org.scalatestplus.junit.JUnitRunner import java.nio.file.{Files, Path} -@RunWith(classOf[JUnitRunner]) class GitObjectTest extends AnyWordSpec with Matchers with Log { private val dir = Path.of("..").toAbsolutePath //TODO fix diff --git a/git/src/test/scala/test/dev/rudiments/git/PackTest.scala b/git/src/test/scala/test/dev/rudiments/git/PackTest.scala index c61b6373..414185f6 100644 --- a/git/src/test/scala/test/dev/rudiments/git/PackTest.scala +++ b/git/src/test/scala/test/dev/rudiments/git/PackTest.scala @@ -3,15 +3,12 @@ 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.junit.runner.RunWith import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec -import org.scalatestplus.junit.JUnitRunner import java.nio.ByteBuffer import java.nio.file.{Files, Path} -@RunWith(classOf[JUnitRunner]) class PackTest extends AnyWordSpec with Matchers with Log { private val dir = Path.of("..").toAbsolutePath //TODO fix diff --git a/git/src/test/scala/test/dev/rudiments/git/RepositoryTest.scala b/git/src/test/scala/test/dev/rudiments/git/RepositoryTest.scala index a46c20c4..d530e8e2 100644 --- a/git/src/test/scala/test/dev/rudiments/git/RepositoryTest.scala +++ b/git/src/test/scala/test/dev/rudiments/git/RepositoryTest.scala @@ -3,14 +3,11 @@ package test.dev.rudiments.git import dev.rudiments.git.{Pack, Repository} import dev.rudiments.git.Pack.PackObj import dev.rudiments.utils.Log -import org.junit.runner.RunWith import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec -import org.scalatestplus.junit.JUnitRunner import java.nio.file.{Files, Path} -@RunWith(classOf[JUnitRunner]) class RepositoryTest extends AnyWordSpec with Matchers with Log { private val dir = Path.of("..").toAbsolutePath //TODO fix From 436407683aa16f69b28f8ed83fe2127f5acdccd8 Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 25 Jan 2024 14:00:12 +0400 Subject: [PATCH 59/75] upgrade scala to latest stable 3.3.1 and target jvm to 21 --- .../src/main/groovy/dev.rudiments.scala-conventions.gradle | 4 ++-- core/src/main/scala/dev/rudiments/codecs/MJ.scala | 2 +- core/src/main/scala/dev/rudiments/codecs/MirrorInfo.scala | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/buildSrc/src/main/groovy/dev.rudiments.scala-conventions.gradle b/buildSrc/src/main/groovy/dev.rudiments.scala-conventions.gradle index a5133bf2..70c5caa0 100644 --- a/buildSrc/src/main/groovy/dev.rudiments.scala-conventions.gradle +++ b/buildSrc/src/main/groovy/dev.rudiments.scala-conventions.gradle @@ -13,7 +13,7 @@ group = 'dev.rudiments' version = baseVersion dependencies { - implementation 'org.scala-lang:scala3-library_3:3.2.2' + implementation 'org.scala-lang:scala3-library_3:3.3.1' implementation 'org.slf4j:slf4j-api:2.0.11' @@ -29,7 +29,7 @@ dependencies { java { toolchain { - languageVersion = JavaLanguageVersion.of(17) + languageVersion = JavaLanguageVersion.of(21) } } diff --git a/core/src/main/scala/dev/rudiments/codecs/MJ.scala b/core/src/main/scala/dev/rudiments/codecs/MJ.scala index 5b13ea2a..f86b5257 100644 --- a/core/src/main/scala/dev/rudiments/codecs/MJ.scala +++ b/core/src/main/scala/dev/rudiments/codecs/MJ.scala @@ -44,7 +44,7 @@ object MJ extends Log { case _: (t *: ts) => summonEncoder[t] :: summonEncodersRec[ts] } - inline final def derived[A](using inline A: Mirror.Of[A]): En[A] = { + 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 diff --git a/core/src/main/scala/dev/rudiments/codecs/MirrorInfo.scala b/core/src/main/scala/dev/rudiments/codecs/MirrorInfo.scala index 8a4fb8f9..3dacbe24 100644 --- a/core/src/main/scala/dev/rudiments/codecs/MirrorInfo.scala +++ b/core/src/main/scala/dev/rudiments/codecs/MirrorInfo.scala @@ -27,7 +27,7 @@ object MirrorInfo { case _: (t *: ts) => constValue[t].asInstanceOf[String] :: summonLabelsRec[ts] } - inline final def apply[A](using inline A: Mirror.Of[A]): MirrorInfo[A] = { + 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] From 205b3e96a50cbf981f6bab7dfc5bea7dfa660ad7 Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 25 Jan 2024 14:33:19 +0400 Subject: [PATCH 60/75] fix ci --- .github/codecov.yml | 22 ------------------- .github/workflows/build-and-test.yml | 8 +++---- .github/workflows/coverage.yml | 18 ++++++++------- .github/workflows/publish-snapshots.yml | 6 ++--- README.md | 2 +- .../dev.rudiments.scala-conventions.gradle | 11 ++++++++++ 6 files changed, 29 insertions(+), 38 deletions(-) delete mode 100644 .github/codecov.yml 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..7fa5636d 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -10,11 +10,11 @@ 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 + 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..16774b1b 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -12,16 +12,18 @@ 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 + 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 + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + directory: ./build/reports/scoverage + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_ORG_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/publish-snapshots.yml b/.github/workflows/publish-snapshots.yml index 354998b1..7bb2d9e0 100644 --- a/.github/workflows/publish-snapshots.yml +++ b/.github/workflows/publish-snapshots.yml @@ -9,10 +9,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up JDK 17 - uses: actions/setup-java@v1 + - name: Set up JDK 21 + uses: actions/setup-java@v4 with: - java-version: 17 + 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/buildSrc/src/main/groovy/dev.rudiments.scala-conventions.gradle b/buildSrc/src/main/groovy/dev.rudiments.scala-conventions.gradle index 70c5caa0..cd63eecf 100644 --- a/buildSrc/src/main/groovy/dev.rudiments.scala-conventions.gradle +++ b/buildSrc/src/main/groovy/dev.rudiments.scala-conventions.gradle @@ -1,5 +1,7 @@ plugins { + id 'java' id 'scala' + id 'jacoco' } def baseVersion = '0.6-SNAPSHOT' @@ -41,3 +43,12 @@ test { } } } + +jacocoTestReport { + reports { + xml.required = true + html.required = false + } +} + +check.dependsOn jacocoTestReport From 65a0ae663ad292054c6c3eb308d8d4d299cc2a78 Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 25 Jan 2024 14:38:27 +0400 Subject: [PATCH 61/75] fix java distribution --- .github/workflows/build-and-test.yml | 1 + .github/workflows/coverage.yml | 1 + .github/workflows/publish-snapshots.yml | 5 +++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 7fa5636d..38402b0f 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -14,6 +14,7 @@ jobs: - name: Set up JDK 21 uses: actions/setup-java@v3 with: + distribution: 'temurin' java-version: 21 - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 16774b1b..a9f39f26 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -16,6 +16,7 @@ jobs: - name: Set up JDK 21 uses: actions/setup-java@v3 with: + distribution: 'temurin' java-version: 21 - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/.github/workflows/publish-snapshots.yml b/.github/workflows/publish-snapshots.yml index 7bb2d9e0..9e179107 100644 --- a/.github/workflows/publish-snapshots.yml +++ b/.github/workflows/publish-snapshots.yml @@ -8,10 +8,11 @@ jobs: push-snapshots: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v3 with: + distribution: 'temurin' java-version: 21 - name: Grant execute permission for gradlew run: chmod +x gradlew From 1caa80bbd490e8c846a39eed7c1d02eb833970d4 Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 25 Jan 2024 14:41:56 +0400 Subject: [PATCH 62/75] fix workflow --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index a9f39f26..d95afbba 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -27,4 +27,4 @@ jobs: with: directory: ./build/reports/scoverage env: - CODECOV_TOKEN: ${{ secrets.CODECOV_ORG_TOKEN }} \ No newline at end of file + CODECOV_TOKEN: ${{ secrets.CODECOV_ORG_TOKEN }} From 0370c4789a61a424051b3b563c7dcc49163028d1 Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 25 Jan 2024 14:45:40 +0400 Subject: [PATCH 63/75] fix workflow --- .github/workflows/coverage.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index d95afbba..14bd2ba1 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -24,7 +24,5 @@ jobs: run: ./gradlew build test - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 - with: - directory: ./build/reports/scoverage - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_ORG_TOKEN }} + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_ORG_TOKEN }} From ed7a1505ac9b2397fae3a5773223410aa9f7c5b4 Mon Sep 17 00:00:00 2001 From: gennady Date: Thu, 25 Jan 2024 14:50:30 +0400 Subject: [PATCH 64/75] try to fix coverage report --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 14bd2ba1..4ad34dbc 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -21,7 +21,7 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build, Test, Generate Scoverage Report with Gradle - run: ./gradlew build test + run: ./gradlew build test check - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 env: From d19bbcd8f1f012b50a0a004f41bb8f6de78bc7b5 Mon Sep 17 00:00:00 2001 From: gennady Date: Fri, 26 Jan 2024 11:32:08 +0400 Subject: [PATCH 65/75] try to fix coverage report --- .github/workflows/coverage.yml | 4 ++-- build.gradle | 20 +++++++++++++++++++ .../dev.rudiments.scala-conventions.gradle | 1 + 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 build.gradle diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 4ad34dbc..660fbf10 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -21,8 +21,8 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build, Test, Generate Scoverage Report with Gradle - run: ./gradlew build test check + run: ./gradlew clean check - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4-beta env: CODECOV_TOKEN: ${{ secrets.CODECOV_ORG_TOKEN }} diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..aea7dc89 --- /dev/null +++ b/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'dev.rudiments.scala-app-conventions' + id 'jacoco-report-aggregation' +} + +dependencies { + jacocoAggregation project(':example') +} + +reporting { + reports { + testCodeCoverageReport(JacocoCoverageReport) { + testType = TestSuiteType.UNIT_TEST + } + } +} + +tasks.named('check') { + dependsOn tasks.named('testCodeCoverageReport', JacocoReport) +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/dev.rudiments.scala-conventions.gradle b/buildSrc/src/main/groovy/dev.rudiments.scala-conventions.gradle index cd63eecf..19f7c4f3 100644 --- a/buildSrc/src/main/groovy/dev.rudiments.scala-conventions.gradle +++ b/buildSrc/src/main/groovy/dev.rudiments.scala-conventions.gradle @@ -45,6 +45,7 @@ test { } jacocoTestReport { + dependsOn test reports { xml.required = true html.required = false From c99694bb4dd04c6eb392ac8e459b5150c5ef2cea Mon Sep 17 00:00:00 2001 From: gennady Date: Fri, 26 Jan 2024 11:36:36 +0400 Subject: [PATCH 66/75] fix token name --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 660fbf10..9ebd42c8 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -25,4 +25,4 @@ jobs: - name: Upload coverage to Codecov uses: codecov/codecov-action@v4-beta env: - CODECOV_TOKEN: ${{ secrets.CODECOV_ORG_TOKEN }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} From 9fdf721730ec59705016501149d010ff6a8b2262 Mon Sep 17 00:00:00 2001 From: gennady Date: Sun, 25 Aug 2024 16:25:27 +0400 Subject: [PATCH 67/75] optimize imports --- core/src/main/scala/dev/rudiments/codecs/Codec.scala | 2 -- core/src/main/scala/dev/rudiments/hardcore/Graph.scala | 4 ++-- core/src/main/scala/dev/rudiments/utils/Hashed.scala | 2 +- .../test/scala/test/dev/rudiments/codecs/CodecTest.scala | 6 ++---- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/core/src/main/scala/dev/rudiments/codecs/Codec.scala b/core/src/main/scala/dev/rudiments/codecs/Codec.scala index 658bd899..cb841a5e 100644 --- a/core/src/main/scala/dev/rudiments/codecs/Codec.scala +++ b/core/src/main/scala/dev/rudiments/codecs/Codec.scala @@ -1,7 +1,5 @@ package dev.rudiments.codecs -import scala.reflect.ClassTag - 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) diff --git a/core/src/main/scala/dev/rudiments/hardcore/Graph.scala b/core/src/main/scala/dev/rudiments/hardcore/Graph.scala index b8a0ab25..7ccf5c9e 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Graph.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Graph.scala @@ -1,7 +1,7 @@ package dev.rudiments.hardcore -import dev.rudiments.codecs.{Encoder, Result} -import dev.rudiments.hardcore.Graph.{AroundNode, Edge, Edges, Item, JointGraph, SeqGraph} +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], diff --git a/core/src/main/scala/dev/rudiments/utils/Hashed.scala b/core/src/main/scala/dev/rudiments/utils/Hashed.scala index 24e5fbf2..fb05abdf 100644 --- a/core/src/main/scala/dev/rudiments/utils/Hashed.scala +++ b/core/src/main/scala/dev/rudiments/utils/Hashed.scala @@ -3,7 +3,7 @@ package dev.rudiments.utils import java.math.BigInteger import java.nio.charset.StandardCharsets.UTF_8 import java.security.MessageDigest -import java.util.{Base64, HexFormat} +import java.util.HexFormat import scala.collection.immutable.ArraySeq sealed trait Hashed(hash: Seq[Byte]) { diff --git a/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala b/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala index 86c13c91..2ad96a07 100644 --- a/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala +++ b/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala @@ -1,13 +1,11 @@ package test.dev.rudiments.codecs -import dev.rudiments.codecs.{MJ, MirrorInfo, OneWay, TS} import dev.rudiments.codecs.Result.* -import dev.rudiments.hardcore.{EdgeTree, Graph, Many} +import dev.rudiments.codecs.{ MJ, MirrorInfo, TS } import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec -import scala.compiletime.{constValue, erasedValue, error, summonFrom} -import scala.deriving.Mirror +import scala.compiletime.{ constValue, erasedValue, error, summonFrom } class CodecTest extends AnyWordSpec with Matchers { From 005fb53ef6a0c964f32f5045c668cb2dd2ca2c51 Mon Sep 17 00:00:00 2001 From: gennady Date: Sat, 14 Sep 2024 13:20:37 +0400 Subject: [PATCH 68/75] fix imports --- .../main/scala/dev/rudiments/git/ByteUtils.scala | 2 -- .../main/scala/dev/rudiments/git/GitObject.scala | 13 ++++++------- git/src/main/scala/dev/rudiments/git/Pack.scala | 7 ++----- .../main/scala/dev/rudiments/git/Repository.scala | 9 +++------ git/src/main/scala/dev/rudiments/git/Writer.scala | 7 +++---- 5 files changed, 14 insertions(+), 24 deletions(-) diff --git a/git/src/main/scala/dev/rudiments/git/ByteUtils.scala b/git/src/main/scala/dev/rudiments/git/ByteUtils.scala index 3f1464dd..fedbd34a 100644 --- a/git/src/main/scala/dev/rudiments/git/ByteUtils.scala +++ b/git/src/main/scala/dev/rudiments/git/ByteUtils.scala @@ -1,7 +1,5 @@ package dev.rudiments.git -import dev.rudiments.git.Pack.PackObj - import java.nio.ByteBuffer implicit class ByteBufferOps(buff: ByteBuffer) { diff --git a/git/src/main/scala/dev/rudiments/git/GitObject.scala b/git/src/main/scala/dev/rudiments/git/GitObject.scala index d707b057..01d1413e 100644 --- a/git/src/main/scala/dev/rudiments/git/GitObject.scala +++ b/git/src/main/scala/dev/rudiments/git/GitObject.scala @@ -1,17 +1,16 @@ package dev.rudiments.git -import dev.rudiments.git.Commit.Field.{Author, Parent} -import dev.rudiments.utils.{Hashed, SHA1, ZLib} +import dev.rudiments.git.Commit.Field.{ Author, Parent } +import dev.rudiments.utils.{ SHA1, ZLib } import java.lang -import java.lang.{IllegalStateException, StringBuffer} +import java.lang.IllegalStateException import java.nio.ByteBuffer import java.nio.charset.StandardCharsets.UTF_8 -import java.nio.file.{Files, Path} -import java.time.{Instant, LocalDateTime, ZoneId, ZonedDateTime} -import java.time.format.{DateTimeFormatter, DateTimeFormatterBuilder, SignStyle} +import java.nio.file.Path +import java.time.format.{ DateTimeFormatter, DateTimeFormatterBuilder, SignStyle } import java.time.temporal.ChronoField -import scala.collection.immutable.ArraySeq +import java.time.{ Instant, ZoneId, ZonedDateTime } import scala.collection.mutable import scala.util.matching.Regex diff --git a/git/src/main/scala/dev/rudiments/git/Pack.scala b/git/src/main/scala/dev/rudiments/git/Pack.scala index 2364c4fb..d7aca8db 100644 --- a/git/src/main/scala/dev/rudiments/git/Pack.scala +++ b/git/src/main/scala/dev/rudiments/git/Pack.scala @@ -1,14 +1,11 @@ package dev.rudiments.git -import dev.rudiments.utils.{CRC, SHA1, ZLib} +import dev.rudiments.utils.{ CRC, SHA1 } -import java.io.{FileInputStream, InputStream} import java.nio.ByteBuffer import java.nio.charset.StandardCharsets.UTF_8 -import java.nio.file.{Files, Path} -import java.nio.file.StandardOpenOption.READ +import java.nio.file.{ Files, Path } import scala.collection.immutable.ArraySeq -import scala.util.{Failure, Success} case class Pack(objects: List[(SHA1, Pack.Entry)]) { lazy val hashIndex: Map[SHA1, Pack.Entry] = objects.toMap diff --git a/git/src/main/scala/dev/rudiments/git/Repository.scala b/git/src/main/scala/dev/rudiments/git/Repository.scala index e592e046..f3907038 100644 --- a/git/src/main/scala/dev/rudiments/git/Repository.scala +++ b/git/src/main/scala/dev/rudiments/git/Repository.scala @@ -1,12 +1,9 @@ package dev.rudiments.git -import java.nio.charset.StandardCharsets.UTF_8 -import dev.rudiments.git.Pack.{Entry, PackObj} -import dev.rudiments.utils.{Log, SHA1, ZLib} +import dev.rudiments.git.Pack.{ Entry, PackObj } +import dev.rudiments.utils.{ Log, SHA1, ZLib } -import java.nio.file.{Files, Path} -import java.util.stream.Collectors -import scala.jdk.CollectionConverters._ +import java.nio.file.{ Files, Path } import scala.collection.mutable class Repository(root: Path) extends Log { diff --git a/git/src/main/scala/dev/rudiments/git/Writer.scala b/git/src/main/scala/dev/rudiments/git/Writer.scala index 927d4114..fd0968d4 100644 --- a/git/src/main/scala/dev/rudiments/git/Writer.scala +++ b/git/src/main/scala/dev/rudiments/git/Writer.scala @@ -1,13 +1,13 @@ package dev.rudiments.git -import dev.rudiments.utils.{Hashed, ZLib} +import dev.rudiments.utils.ZLib -import java.nio.file.{Files, Path} +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._ + import java.nio.file.StandardOpenOption.* try { val compressed = ZLib.pack(obj.full) Files.write(path, compressed, CREATE_NEW, WRITE) @@ -19,7 +19,6 @@ object Writer { def deleteIfExist(repoDir: Path, obj: GitObject): Status = { val path = repoDir.resolve(obj.objectPath).normalize() - import java.nio.file.StandardOpenOption._ try { Files.deleteIfExists(path) Status.Success From e1d26e126142607475d6d654f08598da0f3ebabe Mon Sep 17 00:00:00 2001 From: gennady Date: Sat, 14 Sep 2024 14:32:30 +0400 Subject: [PATCH 69/75] move example classes up --- .../main/scala/dev/rudiments/codecs/MirrorInfo.scala | 1 + core/src/test/scala/test/dev/rudiments/Sample.scala | 9 +++++++++ .../scala/test/dev/rudiments/codecs/CirceTest.scala | 10 ++-------- .../scala/test/dev/rudiments/codecs/CodecTest.scala | 1 + .../test/scala/test/dev/rudiments/codecs/Sample.scala | 5 ----- 5 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 core/src/test/scala/test/dev/rudiments/Sample.scala delete mode 100644 core/src/test/scala/test/dev/rudiments/codecs/Sample.scala diff --git a/core/src/main/scala/dev/rudiments/codecs/MirrorInfo.scala b/core/src/main/scala/dev/rudiments/codecs/MirrorInfo.scala index 3dacbe24..690a7e54 100644 --- a/core/src/main/scala/dev/rudiments/codecs/MirrorInfo.scala +++ b/core/src/main/scala/dev/rudiments/codecs/MirrorInfo.scala @@ -11,6 +11,7 @@ case class MirrorInfo[A]( 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 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/codecs/CirceTest.scala b/core/src/test/scala/test/dev/rudiments/codecs/CirceTest.scala index bec35b9f..aa876f05 100644 --- a/core/src/test/scala/test/dev/rudiments/codecs/CirceTest.scala +++ b/core/src/test/scala/test/dev/rudiments/codecs/CirceTest.scala @@ -2,17 +2,11 @@ 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 io.circe.{ Codec, Json } +import test.dev.rudiments.Sample class CirceTest extends AnyWordSpec with Matchers { - case class Sample( - a: Int, - b: String, - c: Seq[String] - ) - 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")) diff --git a/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala b/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala index 2ad96a07..66771535 100644 --- a/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala +++ b/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala @@ -4,6 +4,7 @@ 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 } diff --git a/core/src/test/scala/test/dev/rudiments/codecs/Sample.scala b/core/src/test/scala/test/dev/rudiments/codecs/Sample.scala deleted file mode 100644 index 75ad1904..00000000 --- a/core/src/test/scala/test/dev/rudiments/codecs/Sample.scala +++ /dev/null @@ -1,5 +0,0 @@ -package test.dev.rudiments.codecs - -case class Sample(i: Int, s: String) - -case class Example(i: Int, s: Sample) \ No newline at end of file From 345940080bf7b60ff3f5ae99c6d3d3c83f47ced2 Mon Sep 17 00:00:00 2001 From: gennady Date: Sat, 14 Sep 2024 14:32:40 +0400 Subject: [PATCH 70/75] tree draft --- .../dev/rudiments/hardcore/EdgeTree.scala | 31 --------- .../scala/dev/rudiments/hardcore/Tree.scala | 68 +++++++++++++++++++ .../dev/rudiments/hardcore/TreeTest.scala | 67 ++++++++++++++++++ 3 files changed, 135 insertions(+), 31 deletions(-) delete mode 100644 core/src/main/scala/dev/rudiments/hardcore/EdgeTree.scala create mode 100644 core/src/main/scala/dev/rudiments/hardcore/Tree.scala create mode 100644 core/src/test/scala/test/dev/rudiments/hardcore/TreeTest.scala diff --git a/core/src/main/scala/dev/rudiments/hardcore/EdgeTree.scala b/core/src/main/scala/dev/rudiments/hardcore/EdgeTree.scala deleted file mode 100644 index e81246f1..00000000 --- a/core/src/main/scala/dev/rudiments/hardcore/EdgeTree.scala +++ /dev/null @@ -1,31 +0,0 @@ -package dev.rudiments.hardcore - -import dev.rudiments.hardcore.Graph.Edges - -case class EdgeTree[K, +B, +L, +E]( - leafs: Map[List[K], L], - branches: Map[List[K], B], - edges: Edges[List[K], E] -) { - lazy val edgesFrom: Map[List[K], Edges[List[K], E]] = edges.groupBy(_.from) - lazy val edgesTo: Map[List[K], Edges[List[K], E]] = edges.groupBy(_.to) - - if (leafs.keySet.intersect(branches.keySet).nonEmpty) { - throw new IllegalArgumentException(s"Branches and leafs have intersecting keys: ${leafs.keySet.intersect(branches.keySet)}") - } - if (((edgesFrom.keySet ++ edgesTo.keySet) -- (leafs.keySet ++ branches.keySet)).nonEmpty) { - throw new IllegalArgumentException(s"Edges have external keys") - } - - def toGraph[E2 >: E](defaultEdge: B => E2): Graph[List[K], B | L, E] = { - val defaultEdges = branches.map { case (k, v) => Graph.Edge(k, k, defaultEdge(v).asInstanceOf[E]) }.toSeq - Graph( - branches ++ leafs, - defaultEdges ++ edges - ) - } -} - -object EdgeTree { - -} \ 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..f99fb2bb --- /dev/null +++ b/core/src/main/scala/dev/rudiments/hardcore/Tree.scala @@ -0,0 +1,68 @@ +package dev.rudiments.hardcore + +import scala.reflect.ClassTag + + +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] + + 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, L | T] = items.toMap + + + + // Search + + def deep(implicit tK: ClassTag[K], tB: ClassTag[B], tL: ClassTag[L], tC: ClassTag[T]): Seq[Item] = { + val rootK = List.empty[K] + Seq(rootK -> self) ++ this.deep(rootK) + } + + def deep(path: List[K])(implicit tK: ClassTag[K], tB: ClassTag[B], tL: ClassTag[L], tC: ClassTag[T]): Seq[Item] = { + items.flatMap { + case (k: K, t: T) => Seq((path :+ k) -> t.self) ++ 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(implicit tK: ClassTag[K], tB: ClassTag[B], tL: ClassTag[L], tC: ClassTag[T]): Seq[Item] = { + val rootK = List.empty[K] + Seq(rootK -> self) ++ this.wide(rootK) + } + + def wide(path: List[K])(implicit tK: ClassTag[K], tB: ClassTag[B], tL: ClassTag[L], tC: ClassTag[T]): Seq[Item] = { + items.map { + case (k, t: T) => (path :+ k) -> 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[K, B, L]) => 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) +} \ No newline at end of file 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..7f74615d --- /dev/null +++ b/core/src/test/scala/test/dev/rudiments/hardcore/TreeTest.scala @@ -0,0 +1,67 @@ +package test.dev.rudiments.hardcore + +import dev.rudiments.hardcore.Tree +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 make a deep search in nested tree" 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 tree" 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", + )) + } +} From 6bb5f74defd6da53bd1ff54ee9e96e27d4d23bd6 Mon Sep 17 00:00:00 2001 From: gennady Date: Sat, 14 Sep 2024 14:37:30 +0400 Subject: [PATCH 71/75] fix tests --- .../scala/test/dev/rudiments/codecs/CodecTest.scala | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala b/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala index 66771535..9a041702 100644 --- a/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala +++ b/core/src/test/scala/test/dev/rudiments/codecs/CodecTest.scala @@ -44,14 +44,17 @@ class CodecTest extends AnyWordSpec with Matchers { } "can derive int and string fields of a case class and recursively" in { - MirrorInfo[Sample] should be ( - MirrorInfo[Sample]("Sample", Seq("i" -> MirrorInfo.intInfo, "s" -> MirrorInfo.strInfo)) - ) + 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" -> MirrorInfo[Sample]("Sample", Seq("i" -> MirrorInfo.intInfo, "s" -> MirrorInfo.strInfo)) + "s" -> sampleShouldBe )) ) } From a08e587f1bb21c925aa93fd3210c1dfc1ae664ea Mon Sep 17 00:00:00 2001 From: gennady Date: Sat, 14 Sep 2024 15:21:27 +0400 Subject: [PATCH 72/75] read by keys --- .../scala/dev/rudiments/hardcore/Tree.scala | 22 ++++++++++ .../dev/rudiments/hardcore/TreeTest.scala | 41 ++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/core/src/main/scala/dev/rudiments/hardcore/Tree.scala b/core/src/main/scala/dev/rudiments/hardcore/Tree.scala index f99fb2bb..14522651 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Tree.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Tree.scala @@ -1,5 +1,7 @@ package dev.rudiments.hardcore +import dev.rudiments.hardcore.TreeError.LeafOnTheWay + import scala.reflect.ClassTag @@ -28,6 +30,21 @@ case class Tree[K, B, L]( val index: Map[K, L | T] = items.toMap + def read(keys: List[K]): Either[TreeError, L | T] = keys match { + case Nil => Right(this) + case h :: Nil => readItem(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(TreeError.NotFound(keys)) + } + } + + def readItem(k: K): Either[TreeError, L | T] = this.index.get(k) match { + case Some(t@Tree(_, _)) => Right(t) + case Some(l: L) => Right(l) + case None => Left(TreeError.NotFound(k :: Nil)) + } // Search @@ -65,4 +82,9 @@ 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) +} + +enum TreeError { + case NotFound[K](path: List[K]) + case LeafOnTheWay[K](k: K, path: List[K]) } \ No newline at end of file diff --git a/core/src/test/scala/test/dev/rudiments/hardcore/TreeTest.scala b/core/src/test/scala/test/dev/rudiments/hardcore/TreeTest.scala index 7f74615d..89e4b717 100644 --- a/core/src/test/scala/test/dev/rudiments/hardcore/TreeTest.scala +++ b/core/src/test/scala/test/dev/rudiments/hardcore/TreeTest.scala @@ -1,6 +1,6 @@ package test.dev.rudiments.hardcore -import dev.rudiments.hardcore.Tree +import dev.rudiments.hardcore.{ Tree, TreeError } import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -33,6 +33,45 @@ class TreeTest extends AnyWordSpec with Matchers { t.items.size should be(4) } + "can read from a nested tree" 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(TreeError.LeafOnTheWay(4, 5 :: Nil))) + t.read(42 :: Nil) should be (Left(TreeError.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 tree" in { t.deep should be(Seq( List.empty[Int] -> (), From ecce2744d765a5740083e94ec97f519d5f9f14df Mon Sep 17 00:00:00 2001 From: gennady Date: Sat, 14 Sep 2024 15:26:36 +0400 Subject: [PATCH 73/75] drop redundant classtags --- .../main/scala/dev/rudiments/hardcore/Tree.scala | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/core/src/main/scala/dev/rudiments/hardcore/Tree.scala b/core/src/main/scala/dev/rudiments/hardcore/Tree.scala index 14522651..e23fd4b4 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Tree.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Tree.scala @@ -2,8 +2,6 @@ package dev.rudiments.hardcore import dev.rudiments.hardcore.TreeError.LeafOnTheWay -import scala.reflect.ClassTag - case class Tree[K, B, L]( self: B, @@ -48,32 +46,32 @@ case class Tree[K, B, L]( // Search - def deep(implicit tK: ClassTag[K], tB: ClassTag[B], tL: ClassTag[L], tC: ClassTag[T]): Seq[Item] = { + def deep: Seq[Item] = { val rootK = List.empty[K] Seq(rootK -> self) ++ this.deep(rootK) } - def deep(path: List[K])(implicit tK: ClassTag[K], tB: ClassTag[B], tL: ClassTag[L], tC: ClassTag[T]): Seq[Item] = { + def deep(path: List[K]): Seq[Item] = { items.flatMap { - case (k: K, t: T) => Seq((path :+ k) -> t.self) ++ t.deep(path :+ k) + 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(implicit tK: ClassTag[K], tB: ClassTag[B], tL: ClassTag[L], tC: ClassTag[T]): Seq[Item] = { + def wide: Seq[Item] = { val rootK = List.empty[K] Seq(rootK -> self) ++ this.wide(rootK) } - def wide(path: List[K])(implicit tK: ClassTag[K], tB: ClassTag[B], tL: ClassTag[L], tC: ClassTag[T]): Seq[Item] = { + def wide(path: List[K]): Seq[Item] = { items.map { - case (k, t: T) => (path :+ k) -> t.self + 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[K, B, L]) => t.wide(path :+ k) + case (k, t@Tree(_, _)) => t.asInstanceOf[T].wide(path :+ k) }.flatten } } From 77dd7e8e1e2c2f1eede67bf2b7e860e9d3567931 Mon Sep 17 00:00:00 2001 From: gennady Date: Sat, 14 Sep 2024 17:12:39 +0400 Subject: [PATCH 74/75] draft messages --- .../scala/dev/rudiments/hardcore/Messages.scala | 17 +++++++++++++++++ .../scala/dev/rudiments/hardcore/Tree.scala | 17 ++++++----------- .../test/dev/rudiments/hardcore/TreeTest.scala | 8 ++++---- 3 files changed, 27 insertions(+), 15 deletions(-) create mode 100644 core/src/main/scala/dev/rudiments/hardcore/Messages.scala 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..d3a896fd --- /dev/null +++ b/core/src/main/scala/dev/rudiments/hardcore/Messages.scala @@ -0,0 +1,17 @@ +package dev.rudiments.hardcore + +sealed trait Message[K] { + val path: List[K] +} + +sealed trait Event[K] extends Message[K] {} + +case class Created[K, B, L](path: List[K], value: L | Tree[K, B, L]) extends Event[K] +case class Updated[K, B, L](path: List[K], old: L | Tree[K, B, L], value: L | Tree[K, B, L]) extends Event[K] +case class Deleted[K, B, L](path: List[K], old: L | Tree[K, B, L]) extends Event[K] +case class Same[K, B, L](path: List[K], value: L | Tree[K, B, L]) extends Event[K] +case class Commit[K](path: List[K], events: Seq[Event[K]]) extends Event[K] + +sealed trait Error[K] extends Message[K] {} +case class NotFound[K](path: List[K]) extends Error[K] +case class LeafOnTheWay[K](k: K, path: List[K]) extends Error[K] diff --git a/core/src/main/scala/dev/rudiments/hardcore/Tree.scala b/core/src/main/scala/dev/rudiments/hardcore/Tree.scala index e23fd4b4..e10fcd37 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Tree.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Tree.scala @@ -1,7 +1,5 @@ package dev.rudiments.hardcore -import dev.rudiments.hardcore.TreeError.LeafOnTheWay - case class Tree[K, B, L]( self: B, @@ -28,22 +26,24 @@ case class Tree[K, B, L]( val index: Map[K, L | T] = items.toMap - def read(keys: List[K]): Either[TreeError, L | T] = keys match { + def read(keys: List[K]): Either[Error[K], L | T] = keys match { case Nil => Right(this) case h :: Nil => readItem(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(TreeError.NotFound(keys)) + case None => Left(NotFound(keys)) } } - def readItem(k: K): Either[TreeError, L | T] = this.index.get(k) match { + def readItem(k: K): Either[Error[K], L | T] = this.index.get(k) match { case Some(t@Tree(_, _)) => Right(t) case Some(l: L) => Right(l) - case None => Left(TreeError.NotFound(k :: Nil)) + case None => Left(NotFound(k :: Nil)) } +// def recons(other: T): + // Search def deep: Seq[Item] = { @@ -81,8 +81,3 @@ object Tree { 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) } - -enum TreeError { - case NotFound[K](path: List[K]) - case LeafOnTheWay[K](k: K, path: List[K]) -} \ No newline at end of file diff --git a/core/src/test/scala/test/dev/rudiments/hardcore/TreeTest.scala b/core/src/test/scala/test/dev/rudiments/hardcore/TreeTest.scala index 89e4b717..a8bdca22 100644 --- a/core/src/test/scala/test/dev/rudiments/hardcore/TreeTest.scala +++ b/core/src/test/scala/test/dev/rudiments/hardcore/TreeTest.scala @@ -1,6 +1,6 @@ package test.dev.rudiments.hardcore -import dev.rudiments.hardcore.{ Tree, TreeError } +import dev.rudiments.hardcore.{ LeafOnTheWay, NotFound, Tree } import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -8,7 +8,7 @@ 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) + t.items shouldBe empty } var t: Tree[Int, Unit, String] = _ @@ -38,8 +38,8 @@ class TreeTest extends AnyWordSpec with Matchers { 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(TreeError.LeafOnTheWay(4, 5 :: Nil))) - t.read(42 :: Nil) should be (Left(TreeError.NotFound(42 :: Nil))) + 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" From ea90d827365744c528b1a2361dc8c44560ee1ba5 Mon Sep 17 00:00:00 2001 From: gennady Date: Sun, 15 Sep 2024 16:08:51 +0400 Subject: [PATCH 75/75] draft crud on a tree --- .../dev/rudiments/hardcore/Messages.scala | 29 +++-- .../scala/dev/rudiments/hardcore/Tree.scala | 35 +++++- .../dev/rudiments/hardcore/TreeTest.scala | 103 +++++++++++++++++- 3 files changed, 146 insertions(+), 21 deletions(-) diff --git a/core/src/main/scala/dev/rudiments/hardcore/Messages.scala b/core/src/main/scala/dev/rudiments/hardcore/Messages.scala index d3a896fd..ba3a1694 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Messages.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Messages.scala @@ -1,17 +1,22 @@ package dev.rudiments.hardcore -sealed trait Message[K] { - val path: List[K] -} +sealed trait Message {} + +sealed trait Event extends Message {} -sealed trait Event[K] extends Message[K] {} +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 -case class Created[K, B, L](path: List[K], value: L | Tree[K, B, L]) extends Event[K] -case class Updated[K, B, L](path: List[K], old: L | Tree[K, B, L], value: L | Tree[K, B, L]) extends Event[K] -case class Deleted[K, B, L](path: List[K], old: L | Tree[K, B, L]) extends Event[K] -case class Same[K, B, L](path: List[K], value: L | Tree[K, B, L]) extends Event[K] -case class Commit[K](path: List[K], events: Seq[Event[K]]) extends Event[K] +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 -sealed trait Error[K] extends Message[K] {} -case class NotFound[K](path: List[K]) extends Error[K] -case class LeafOnTheWay[K](k: K, path: List[K]) extends Error[K] +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/Tree.scala b/core/src/main/scala/dev/rudiments/hardcore/Tree.scala index e10fcd37..29bece0e 100644 --- a/core/src/main/scala/dev/rudiments/hardcore/Tree.scala +++ b/core/src/main/scala/dev/rudiments/hardcore/Tree.scala @@ -8,6 +8,7 @@ case class 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 { @@ -23,12 +24,12 @@ case class Tree[K, B, L]( s"Branches and leaves have intersecting keys: ${leaves.keySet.intersect(branches.keySet)}" ) - val index: Map[K, L | T] = items.toMap + val index: Map[K, N] = items.toMap - def read(keys: List[K]): Either[Error[K], L | T] = keys match { + def read(keys: List[K]): Either[Error, N] = keys match { case Nil => Right(this) - case h :: Nil => readItem(h) + 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)) @@ -36,13 +37,37 @@ case class Tree[K, B, L]( } } - def readItem(k: K): Either[Error[K], L | T] = this.index.get(k) match { + 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 recons(other: T): + 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 diff --git a/core/src/test/scala/test/dev/rudiments/hardcore/TreeTest.scala b/core/src/test/scala/test/dev/rudiments/hardcore/TreeTest.scala index a8bdca22..175db930 100644 --- a/core/src/test/scala/test/dev/rudiments/hardcore/TreeTest.scala +++ b/core/src/test/scala/test/dev/rudiments/hardcore/TreeTest.scala @@ -1,6 +1,6 @@ package test.dev.rudiments.hardcore -import dev.rudiments.hardcore.{ LeafOnTheWay, NotFound, Tree } +import dev.rudiments.hardcore.{ Created, Deleted, LeafOnTheWay, NotFound, Tree, Updated } import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -33,7 +33,7 @@ class TreeTest extends AnyWordSpec with Matchers { t.items.size should be(4) } - "can read from a nested tree" in { + "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")) @@ -72,7 +72,7 @@ class TreeTest extends AnyWordSpec with Matchers { ))) } - "can make a deep search in nested tree" in { + "can make a deep search in nested trees" in { t.deep should be(Seq( List.empty[Int] -> (), List(1) -> "a", List(2) -> "b", @@ -88,7 +88,7 @@ class TreeTest extends AnyWordSpec with Matchers { )) } - "can make a wide search in nested tree" in { + "can make a wide search in nested trees" in { t.wide should be (Seq( List.empty[Int] -> (), List(1) -> "a", List(2) -> "b", @@ -103,4 +103,99 @@ class TreeTest extends AnyWordSpec with Matchers { 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" + )) + } }