diff --git a/src/main/scala/fr/loicknuchel/safeql/gen/Generator.scala b/src/main/scala/fr/loicknuchel/safeql/gen/Generator.scala index 17e1673..30462c3 100644 --- a/src/main/scala/fr/loicknuchel/safeql/gen/Generator.scala +++ b/src/main/scala/fr/loicknuchel/safeql/gen/Generator.scala @@ -93,4 +93,19 @@ object Generator { database <- reader.read() _ <- writer.write(database).toIO } yield () + + /** + * Allow to start with writer + */ + + def writer(writer: Writer) = new Builder(writer) + + class Builder(writer: Writer) { + def flyway(flywayLocations: String*): FlywayGenerator = Generator.flyway(flywayLocations: _*).writer(writer) + + def fromFiles(paths: List[String]): SQLFilesGenerator = Generator.fromFiles(paths).writer(writer) + + def reader(reader: Reader): ReaderGenerator = Generator.reader(reader).writer(writer) + } + } diff --git a/src/main/scala/fr/loicknuchel/safeql/gen/reader/H2Reader.scala b/src/main/scala/fr/loicknuchel/safeql/gen/reader/H2Reader.scala index b10addd..ad5e4db 100644 --- a/src/main/scala/fr/loicknuchel/safeql/gen/reader/H2Reader.scala +++ b/src/main/scala/fr/loicknuchel/safeql/gen/reader/H2Reader.scala @@ -12,21 +12,29 @@ import scala.concurrent.ExecutionContext class H2Reader(val url: String, val user: String, val pass: String, - schema: Option[String], - excludes: Option[String]) extends Reader { + val schema: Option[String], + val excludes: Option[String]) extends Reader { val driver: String = "org.h2.Driver" protected[gen] lazy val xa: doobie.Transactor[IO] = { implicit val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global) Transactor.fromDriverManager[IO](driver, url, user, pass) } + def url(u: String): H2Reader = new H2Reader(u, user, pass, schema, excludes) + + def user(u: String): H2Reader = new H2Reader(url, u, pass, schema, excludes) + + def pass(p: String): H2Reader = new H2Reader(url, user, p, schema, excludes) + + def schema(s: String): H2Reader = new H2Reader(url, user, pass, schema = Some(s), excludes) + + def excludes(regex: String): H2Reader = new H2Reader(url, user, pass, schema, excludes = Some(regex)) + override def read(): IO[Database] = for { columns <- readColumns(xa) crossReferences <- readCrossReferences(xa) } yield buildDatabase(columns, crossReferences) - def excludes(regex: String): H2Reader = new H2Reader(url, user, pass, schema, excludes = Some(regex)) - protected def buildDatabase(columns: List[Column], crossReferences: List[CrossReference]): Database = { val refs = crossReferences.map(r => (Database.FieldRef(r.FKTABLE_SCHEMA, r.FKTABLE_NAME, r.FKCOLUMN_NAME), Database.FieldRef(r.PKTABLE_SCHEMA, r.PKTABLE_NAME, r.PKCOLUMN_NAME))).toMap Database(columns.groupBy(_.TABLE_SCHEMA).toList.sortBy(_._1).map { case (schema, tables) => diff --git a/src/main/scala/fr/loicknuchel/safeql/gen/writer/ScalaWriter.scala b/src/main/scala/fr/loicknuchel/safeql/gen/writer/ScalaWriter.scala index ca7c1c4..9784dbe 100644 --- a/src/main/scala/fr/loicknuchel/safeql/gen/writer/ScalaWriter.scala +++ b/src/main/scala/fr/loicknuchel/safeql/gen/writer/ScalaWriter.scala @@ -8,18 +8,24 @@ import fr.loicknuchel.safeql.gen.writer.ScalaWriter.{DatabaseConfig, TableConfig import fr.loicknuchel.safeql.gen.writer.Writer.IdentifierStrategy import fr.loicknuchel.safeql.utils.StringUtils -class ScalaWriter(directory: String, - packageName: String, - identifierStrategy: IdentifierStrategy, - config: DatabaseConfig) extends Writer { +class ScalaWriter(val directory: String, + val packageName: String, + val identifierStrategy: IdentifierStrategy, + val config: DatabaseConfig) extends Writer { require(config.getConfigErrors.isEmpty, s"DatabaseConfig has some errors :${config.getConfigErrors.map("\n - " + _).mkString}") require(StringUtils.isScalaPackage(packageName), s"'$packageName' is an invalid scala package name") def directory(dir: String): ScalaWriter = new ScalaWriter(dir, packageName, identifierStrategy, config) - override protected def getDatabaseErrors(db: Database): List[String] = config.getDatabaseErrors(db) + def packageName(pkg: String): ScalaWriter = new ScalaWriter(directory, pkg, identifierStrategy, config) - override protected[gen] def rootFolderPath: String = directory + "/" + packageName.replaceAll("\\.", "/") + def identifierStrategy(idf: IdentifierStrategy): ScalaWriter = new ScalaWriter(directory, packageName, idf, config) + + def config(conf: DatabaseConfig): ScalaWriter = new ScalaWriter(directory, packageName, identifierStrategy, conf) + + override def getDatabaseErrors(db: Database): List[String] = config.getDatabaseErrors(db) + + override def rootFolderPath: String = directory + "/" + packageName.replaceAll("\\.", "/") override protected[writer] def tableFilePath(t: Table): String = tablesFolderPath + "/" + idf(t.name) + ".scala" diff --git a/src/main/scala/fr/loicknuchel/safeql/models/Page.scala b/src/main/scala/fr/loicknuchel/safeql/models/Page.scala index d632ac8..d81e21e 100644 --- a/src/main/scala/fr/loicknuchel/safeql/models/Page.scala +++ b/src/main/scala/fr/loicknuchel/safeql/models/Page.scala @@ -28,6 +28,10 @@ object Page { def filters(f: Map[String, String]): Params = copy(filters = f) def filters(f: (String, String)*): Params = filters(f.toMap) + + def withNullsFirst: Params = copy(nullsFirst = true) + + def withNullsLast: Params = copy(nullsFirst = false) } } diff --git a/src/main/scala/fr/loicknuchel/safeql/utils/Extensions.scala b/src/main/scala/fr/loicknuchel/safeql/utils/Extensions.scala index 20f40ca..02f4c67 100644 --- a/src/main/scala/fr/loicknuchel/safeql/utils/Extensions.scala +++ b/src/main/scala/fr/loicknuchel/safeql/utils/Extensions.scala @@ -13,7 +13,7 @@ import scala.collection.mutable import scala.util.control.NonFatal import scala.util.{Failure, Success, Try} -object Extensions { +private[safeql] object Extensions { implicit class RichOption[A](val in: Option[A]) extends AnyVal { def toEither[E](e: => E): Either[E, A] = in match { diff --git a/src/main/scala/fr/loicknuchel/safeql/utils/FileUtils.scala b/src/main/scala/fr/loicknuchel/safeql/utils/FileUtils.scala index a9f7362..cfa8a5a 100644 --- a/src/main/scala/fr/loicknuchel/safeql/utils/FileUtils.scala +++ b/src/main/scala/fr/loicknuchel/safeql/utils/FileUtils.scala @@ -3,10 +3,12 @@ package fr.loicknuchel.safeql.utils import java.io.File import java.nio.file.{Files, Paths} +import fr.loicknuchel.safeql.utils.Extensions._ + import scala.jdk.CollectionConverters._ import scala.util.Try -object FileUtils { +private[safeql] object FileUtils { def parent(path: String): String = path.split("/").dropRight(1).mkString("/") @@ -22,6 +24,13 @@ object FileUtils { listDir(new File(path)).filter(_.isFile).map(_.getPath).sorted } + // return a map for files with their relative path inside the directory and their content + def getDirContent(path: String): Try[Map[String, String]] = { + FileUtils.listFiles(path) + .flatMap(_.map(p => FileUtils.read(p).map(c => (p.stripPrefix(path + "/"), c))).sequence) + .map(_.toMap) + } + def read(path: String): Try[String] = Try(Files.readAllLines(Paths.get(path))).map(_.asScala.mkString("\n")) diff --git a/src/main/scala/fr/loicknuchel/safeql/utils/StringUtils.scala b/src/main/scala/fr/loicknuchel/safeql/utils/StringUtils.scala index 64893db..74ef579 100644 --- a/src/main/scala/fr/loicknuchel/safeql/utils/StringUtils.scala +++ b/src/main/scala/fr/loicknuchel/safeql/utils/StringUtils.scala @@ -2,7 +2,7 @@ package fr.loicknuchel.safeql.utils import java.text.Normalizer -object StringUtils { +private[safeql] object StringUtils { def removeDiacritics(str: String): String = Normalizer.normalize(str, Normalizer.Form.NFD) .replaceAll("\\p{InCombiningDiacriticalMarks}+", "") diff --git a/src/test/scala/fr/loicknuchel/safeql/gen/GeneratorSpec.scala b/src/test/scala/fr/loicknuchel/safeql/gen/GeneratorSpec.scala index 0a2228a..6a7f360 100644 --- a/src/test/scala/fr/loicknuchel/safeql/gen/GeneratorSpec.scala +++ b/src/test/scala/fr/loicknuchel/safeql/gen/GeneratorSpec.scala @@ -2,82 +2,52 @@ package fr.loicknuchel.safeql.gen import java.util.UUID -import cats.data.NonEmptyList import fr.loicknuchel.safeql.gen.reader.H2Reader -import fr.loicknuchel.safeql.gen.writer.ScalaWriter.{DatabaseConfig, FieldConfig, SchemaConfig, TableConfig} -import fr.loicknuchel.safeql.gen.writer.{ScalaWriter, Writer} -import fr.loicknuchel.safeql.testingutils.BaseSpec -import fr.loicknuchel.safeql.utils.Extensions._ +import fr.loicknuchel.safeql.testingutils.{BaseSpec, CLI} import fr.loicknuchel.safeql.utils.FileUtils import org.flywaydb.core.Flyway import org.flywaydb.core.internal.jdbc.DriverDataSource import org.scalatest.BeforeAndAfterEach -import scala.util.Try - class GeneratorSpec extends BaseSpec with BeforeAndAfterEach { - private val root = "target/tmp-generator-tests" - private val reader = H2Reader( - url = s"jdbc:h2:mem:${UUID.randomUUID()};MODE=PostgreSQL;DATABASE_TO_UPPER=false;DB_CLOSE_DELAY=-1", - schema = Some("PUBLIC"), - excludes = Some(".*flyway.*")) - private val writer = ScalaWriter( - directory = "src/test/scala", - packageName = "fr.loicknuchel.safeql.testingutils.database", - identifierStrategy = Writer.IdentifierStrategy.upperCase, - config = DatabaseConfig( - scaladoc = _ => Some("Hello"), - imports = List("fr.loicknuchel.safeql.testingutils.Entities._"), - schemas = Map("PUBLIC" -> SchemaConfig(tables = Map( - "users" -> TableConfig(alias = Some("u"), fields = Map( - "id" -> FieldConfig(customType = Some("User.Id")))), - "categories" -> TableConfig(alias = "c", sort = TableConfig.Sort("name", NonEmptyList.of("-name", "id")), search = List("name"), fields = Map( - "id" -> FieldConfig(customType = Some("Category.Id")))), - "posts" -> TableConfig(alias = Some("p"), fields = Map( - "id" -> FieldConfig(customType = Some("Post.Id")))) - ))))) + private val root = "target/tests-generator" override protected def afterEach(): Unit = FileUtils.delete(root).get describe("Generator") { it("should generate the same files with all the generators") { // Basic generation + val reader = H2Reader( + url = s"jdbc:h2:mem:${UUID.randomUUID()};MODE=PostgreSQL;DATABASE_TO_UPPER=false;DB_CLOSE_DELAY=-1", + schema = Some("PUBLIC"), + excludes = Some(".*flyway.*")) Flyway.configure() .dataSource(new DriverDataSource(this.getClass.getClassLoader, reader.driver, reader.url, reader.user, reader.pass)) .locations("classpath:sql_migrations") .load().migrate() val basicPath = s"$root/basic-gen" - Generator.reader(reader).writer(writer.directory(basicPath)).generate().unsafeRunSync() - val basicDb = getFolderContent(basicPath).get + Generator.reader(reader).writer(CLI.GenerateSampleDatabase.writer.directory(basicPath)).generate().unsafeRunSync() + val basicDb = FileUtils.getDirContent(basicPath).get // Flyway generator val flywapPath = s"$root/flyway-gen" - Generator.flyway("classpath:sql_migrations").writer(writer.directory(flywapPath)).generate().unsafeRunSync() - val flywayDb = getFolderContent(flywapPath).get + Generator.flyway("classpath:sql_migrations").writer(CLI.GenerateSampleDatabase.writer.directory(flywapPath)).generate().unsafeRunSync() + val flywayDb = FileUtils.getDirContent(flywapPath).get flywayDb shouldBe basicDb // SQL files generator val sqlFilesPath = s"$root/sql-gen" - Generator.fromFiles(List("src/test/resources/sql_migrations/V1__test_schema.sql")).writer(writer.directory(sqlFilesPath)).generate().unsafeRunSync() - val sqlFilesDb = getFolderContent(sqlFilesPath).get + Generator.fromFiles(List("src/test/resources/sql_migrations/V1__test_schema.sql")).writer(CLI.GenerateSampleDatabase.writer.directory(sqlFilesPath)).generate().unsafeRunSync() + val sqlFilesDb = FileUtils.getDirContent(sqlFilesPath).get sqlFilesDb shouldBe basicDb } it("should keep the generated database up to date") { - val flywayWriter = writer.directory(s"$root/flyway-gen") + val flywayWriter = CLI.GenerateSampleDatabase.writer.directory(s"$root/flyway-gen") Generator.flyway("classpath:sql_migrations").writer(flywayWriter).generate().unsafeRunSync() - val flywayDb = getFolderContent(flywayWriter.rootFolderPath).get - val currentDb = getFolderContent(writer.rootFolderPath).get + val flywayDb = FileUtils.getDirContent(flywayWriter.rootFolderPath).get + val currentDb = FileUtils.getDirContent(CLI.GenerateSampleDatabase.writer.rootFolderPath).get currentDb shouldBe flywayDb } - ignore("should generate the database tables") { // run this test to generate the test database tables - Generator.reader(reader).writer(writer).generate().unsafeRunSync() - } - } - - private def getFolderContent(path: String): Try[Map[String, String]] = { - FileUtils.listFiles(path) - .flatMap(_.map(p => FileUtils.read(p).map(c => (p.stripPrefix(path), c))).sequence) - .map(_.toMap) } } diff --git a/src/test/scala/fr/loicknuchel/safeql/models/ExceptionsSpec.scala b/src/test/scala/fr/loicknuchel/safeql/models/ExceptionsSpec.scala new file mode 100644 index 0000000..4088357 --- /dev/null +++ b/src/test/scala/fr/loicknuchel/safeql/models/ExceptionsSpec.scala @@ -0,0 +1,20 @@ +package fr.loicknuchel.safeql.models + +import fr.loicknuchel.safeql.testingutils.BaseSpec + +class ExceptionsSpec extends BaseSpec { + private val e1 = new Exception("an error") + private val e2 = new Exception("an other error") + private val m = MultiException(e1, e2) + + describe("Exceptions") { + describe("MultiException") { + it("should carry multiple exceptions") { + m.getMessage shouldBe "\n - an error\n - an other error" + m.getLocalizedMessage shouldBe "\n - an error\n - an other error" + m.getStackTrace shouldBe e1.getStackTrace + m.getCause shouldBe e1.getCause + } + } + } +} diff --git a/src/test/scala/fr/loicknuchel/safeql/models/MultiExceptionSpec.scala b/src/test/scala/fr/loicknuchel/safeql/models/MultiExceptionSpec.scala deleted file mode 100644 index 9da9480..0000000 --- a/src/test/scala/fr/loicknuchel/safeql/models/MultiExceptionSpec.scala +++ /dev/null @@ -1,18 +0,0 @@ -package fr.loicknuchel.safeql.models - -import fr.loicknuchel.safeql.testingutils.BaseSpec - -class MultiExceptionSpec extends BaseSpec { - private val e1 = new Exception("an error") - private val e2 = new Exception("an other error") - private val m = MultiException(e1, e2) - - describe("MultiException") { - it("should carry multiple exceptions") { - m.getMessage shouldBe "\n - an error\n - an other error" - m.getLocalizedMessage shouldBe "\n - an error\n - an other error" - m.getStackTrace shouldBe e1.getStackTrace - m.getCause shouldBe e1.getCause - } - } -} diff --git a/src/test/scala/fr/loicknuchel/safeql/models/PageSpec.scala b/src/test/scala/fr/loicknuchel/safeql/models/PageSpec.scala new file mode 100644 index 0000000..d7c9385 --- /dev/null +++ b/src/test/scala/fr/loicknuchel/safeql/models/PageSpec.scala @@ -0,0 +1,43 @@ +package fr.loicknuchel.safeql.models + +import fr.loicknuchel.safeql.testingutils.BaseSpec + +class PageSpec extends BaseSpec { + describe("Page") { + describe("Params") { + it("should have all default values") { + Page.Params().page shouldBe 1 // testing the empty constructor + } + it("should have setters") { + val p = Page.Params() + + p.page shouldBe 1 + p.page(2).page shouldBe 2 + + p.pageSize shouldBe 20 + p.pageSize(2).pageSize shouldBe 2 + + p.search shouldBe None + p.search("q").search shouldBe Some("q") + + p.orderBy shouldBe List() + p.orderBy("name,date", "score").orderBy shouldBe List("name", "date", "score") + + p.filters shouldBe Map() + p.filters("type" -> "meetup", "future" -> "true").filters shouldBe Map("type" -> "meetup", "future" -> "true") + + p.nullsFirst shouldBe false + p.withNullsFirst.nullsFirst shouldBe true + } + it("should clean arguments on orderBy setter") { + val p = Page.Params() + p.orderBy("a,,b ,c", "d,e").orderBy shouldBe List("a", "b", "c", "d", "e") + } + it("should update order by only when empty") { + val p = Page.Params() + p.defaultOrderBy("b").orderBy shouldBe List("b") + p.orderBy("a").defaultOrderBy("b").orderBy shouldBe List("a") + } + } + } +} diff --git a/src/test/scala/fr/loicknuchel/safeql/testingutils/CLI.scala b/src/test/scala/fr/loicknuchel/safeql/testingutils/CLI.scala new file mode 100644 index 0000000..cb3814d --- /dev/null +++ b/src/test/scala/fr/loicknuchel/safeql/testingutils/CLI.scala @@ -0,0 +1,37 @@ +package fr.loicknuchel.safeql.testingutils + +import cats.data.NonEmptyList +import cats.effect.IO +import fr.loicknuchel.safeql.gen.Generator +import fr.loicknuchel.safeql.gen.writer.ScalaWriter.{DatabaseConfig, FieldConfig, SchemaConfig, TableConfig} +import fr.loicknuchel.safeql.gen.writer.{ScalaWriter, Writer} + +object CLI { + def main(args: Array[String]): Unit = { + GenerateSampleDatabase.run().unsafeRunSync() + println("Done") + } + + object GenerateSampleDatabase { + val writer: ScalaWriter = ScalaWriter( + directory = "src/test/scala", + packageName = "fr.loicknuchel.safeql.testingutils.database", + identifierStrategy = Writer.IdentifierStrategy.upperCase, + config = DatabaseConfig( + scaladoc = _ => Some("Hello"), + imports = List("fr.loicknuchel.safeql.testingutils.Entities._"), + schemas = Map("PUBLIC" -> SchemaConfig(tables = Map( + "users" -> TableConfig(alias = Some("u"), fields = Map( + "id" -> FieldConfig(customType = Some("User.Id")))), + "categories" -> TableConfig(alias = "c", sort = TableConfig.Sort("name", NonEmptyList.of("-name", "id")), search = List("name"), fields = Map( + "id" -> FieldConfig(customType = Some("Category.Id")))), + "posts" -> TableConfig(alias = Some("p"), fields = Map( + "id" -> FieldConfig(customType = Some("Post.Id")))) + ))))) + + def run(): IO[Unit] = { + Generator.flyway("classpath:sql_migrations").writer(writer).generate() + } + } + +} diff --git a/src/test/scala/fr/loicknuchel/safeql/utils/FileUtilsSpec.scala b/src/test/scala/fr/loicknuchel/safeql/utils/FileUtilsSpec.scala index fc33e85..4461191 100644 --- a/src/test/scala/fr/loicknuchel/safeql/utils/FileUtilsSpec.scala +++ b/src/test/scala/fr/loicknuchel/safeql/utils/FileUtilsSpec.scala @@ -4,7 +4,7 @@ import fr.loicknuchel.safeql.testingutils.BaseSpec import org.scalatest.BeforeAndAfterEach class FileUtilsSpec extends BaseSpec with BeforeAndAfterEach { - private val root = "target/file-utils-tests" + private val root = "target/tests-file-utils" override protected def beforeEach(): Unit = FileUtils.mkdirs(root).get @@ -35,5 +35,14 @@ class FileUtilsSpec extends BaseSpec with BeforeAndAfterEach { FileUtils.delete(s"$root/src").get an[Exception] should be thrownBy FileUtils.read(s"$root/src/test/scala/fr/lkn/main.scala").get } + it("should list folder content") { + FileUtils.mkdirs(s"$root/src/main/scala/fr/loicknuchel").get + FileUtils.write(s"$root/src/main/scala/fr/loicknuchel/Main.scala", "public class Main").get + FileUtils.write(s"$root/src/main/scala/README.md", "The readme").get + + FileUtils.getDirContent(s"$root/src/main/scala").get shouldBe Map( + "README.md" -> "The readme", + "fr/loicknuchel/Main.scala" -> "public class Main") + } } }