diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68026aff..a4be78bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.10, 2.12.17] + scala: [2.13.10, 2.12.17, 3.3.0] java: - adopt-hotspot@8 - adopt-hotspot@11 @@ -92,7 +92,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.12.17] + scala: [2.12.18] java: [adopt-hotspot@8] runs-on: ${{ matrix.os }} steps: diff --git a/build.sbt b/build.sbt index af348a7a..e5b9ad6f 100644 --- a/build.sbt +++ b/build.sbt @@ -16,11 +16,11 @@ Global / onChangedBuildSource := ReloadOnSourceChanges inThisBuild( Seq( libraryDependencySchemes += "org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always, - organization := "com.github.alonsodomin.cron4s", - organizationName := "Antonio Alonso Dominguez", - description := "CRON expression parser for Scala", - startYear := Some(2017), - crossScalaVersions := Seq("2.13.10", "2.12.17"), + organization := "com.github.alonsodomin.cron4s", + organizationName := "Antonio Alonso Dominguez", + description := "CRON expression parser for Scala", + startYear := Some(2017), + crossScalaVersions := Seq("2.13.10", "2.12.17", "3.3.0"), homepage := Some(url("https://github.com/alonsodomin/cron4s")), licenses += ("Apache-2.0", url("https://www.apache.org/licenses/LICENSE-2.0.txt")), scmInfo := Some( @@ -46,21 +46,26 @@ val commonSettings = Def.settings( "-unchecked", "-deprecation", "-explaintypes", - "-Xlint:-unused,_", "-Xfatal-warnings", "-language:postfixOps", "-language:implicitConversions", "-language:higherKinds", "-language:existentials" ), + scalacOptions ++= (if (scalaVersion.value.startsWith("2.")) { + Seq( + "-Xlint:-unused,_" + ), + } else + Seq( + "-Wunused:imports", + "-Wunused:locals", + "-Wunused:implicits", + "-Wunused:privates" + )), scalacOptions ++= { CrossVersion.partialVersion(scalaVersion.value) match { case Some((2, n)) if n == 12 => Seq("-Ypartial-unification") - case _ => Nil - } - }, - scalacOptions ++= { - CrossVersion.partialVersion(scalaVersion.value) match { case Some((2, n)) if n > 12 => Seq("-Xlint:-byname-implicit", "-Ymacro-annotations") case _ => Nil @@ -70,10 +75,10 @@ val commonSettings = Def.settings( Set("-Xlint:-unused,_", "-Xfatal-warnings") ), Test / console / scalacOptions := (Compile / console / scalacOptions).value, - apiURL := Some(url("https://alonsodomin.github.io/cron4s/api/")), - autoAPIMappings := true, - Test / parallelExecution := false, - consoleImports := Seq("cron4s._"), + apiURL := Some(url("https://alonsodomin.github.io/cron4s/api/")), + autoAPIMappings := true, + Test / parallelExecution := false, + consoleImports := Seq("cron4s._"), console / initialCommands := consoleImports.value .map(s => s"import $s") .mkString("\n") @@ -85,18 +90,19 @@ lazy val commonJvmSettings = Seq( lazy val commonJsSettings = Seq( Global / scalaJSStage := FastOptStage, - scalacOptions += { - val tagOrHash = { - if (isSnapshot.value) - sys.process.Process("git rev-parse HEAD").lineStream_!.head - else version.value - } - val a = (LocalRootProject / baseDirectory).value.toURI.toString - val g = "https://raw.githubusercontent.com/alonsodomin/cron4s/" + tagOrHash - s"-P:scalajs:mapSourceURI:$a->$g/" - }, + scalacOptions ++= (if (scalaVersion.value.startsWith("2.")) Seq { + val tagOrHash = { + if (isSnapshot.value) + sys.process.Process("git rev-parse HEAD").lineStream_!.head + else version.value + } + val a = (LocalRootProject / baseDirectory).value.toURI.toString + val g = "https://raw.githubusercontent.com/alonsodomin/cron4s/" + tagOrHash + s"-P:scalajs:mapSourceURI:$a->$g/" + } + else Seq.empty), scalaJSLinkerConfig := scalaJSLinkerConfig.value.withModuleKind(ModuleKind.CommonJSModule), - jsEnv := new org.scalajs.jsenv.nodejs.NodeJSEnv() + jsEnv := new org.scalajs.jsenv.nodejs.NodeJSEnv() ) lazy val consoleSettings = Seq( @@ -104,8 +110,8 @@ lazy val consoleSettings = Seq( ) lazy val publishSettings = Seq( - sonatypeProfileName := "com.github.alonsodomin", - publishMavenStyle := true, + sonatypeProfileName := "com.github.alonsodomin", + publishMavenStyle := true, Test / publishArtifact := false, // don't include scoverage as a dependency in the pom // see issue #980 @@ -125,17 +131,17 @@ lazy val publishSettings = Seq( ) lazy val noPublishSettings = publishSettings ++ Seq( - publish / skip := true, - publishArtifact := false, + publish / skip := true, + publishArtifact := false, mimaFailOnNoPrevious := false ) lazy val coverageSettings = Seq( - coverageMinimumStmtTotal := 90, + coverageMinimumStmtTotal := 90, coverageMinimumBranchTotal := 80, - coverageFailOnMinimum := true, - coverageHighlighting := true, - coverageExcludedPackages := "cron4s\\.bench\\..*" + coverageFailOnMinimum := true, + coverageHighlighting := true, + coverageExcludedPackages := "cron4s\\.bench\\..*" ) def mimaSettings(module: String): Seq[Setting[_]] = @@ -178,19 +184,19 @@ lazy val docsMappingsAPIDir = settingKey[String]("Name of subdirectory in site target directory for api docs") lazy val docSettings = Seq( - micrositeName := "Cron4s", - micrositeDescription := "Scala CRON Parser", - micrositeHighlightTheme := "atom-one-light", - micrositeAuthor := "Antonio Alonso Dominguez", - micrositeGithubOwner := "alonsodomin", - micrositeGithubRepo := "cron4s", - micrositeGitterChannel := true, - micrositeUrl := "https://www.alonsodomin.me", - micrositeBaseUrl := "/cron4s", - micrositeHomepage := "https://www.alonsodomin.me/cron4s/", - micrositeDocumentationUrl := "/cron4s/api/cron4s/index.html", + micrositeName := "Cron4s", + micrositeDescription := "Scala CRON Parser", + micrositeHighlightTheme := "atom-one-light", + micrositeAuthor := "Antonio Alonso Dominguez", + micrositeGithubOwner := "alonsodomin", + micrositeGithubRepo := "cron4s", + micrositeGitterChannel := true, + micrositeUrl := "https://www.alonsodomin.me", + micrositeBaseUrl := "/cron4s", + micrositeHomepage := "https://www.alonsodomin.me/cron4s/", + micrositeDocumentationUrl := "/cron4s/api/cron4s/index.html", micrositeDocumentationLabelDescription := "API Documentation", - micrositeTwitterCreator := "@_alonsodomin_", + micrositeTwitterCreator := "@_alonsodomin_", micrositeExtraMdFiles := Map( file("CHANGELOG.md") -> ExtraMdFileConfig( "changelog.md", @@ -218,15 +224,15 @@ lazy val docSettings = Seq( "momentjsVersion" -> Dependencies.version.momentjs ) ), - mdocIn := sourceDirectory.value / "main" / "mdoc", - Test / fork := true, + mdocIn := sourceDirectory.value / "main" / "mdoc", + Test / fork := true, docsMappingsAPIDir := "api", addMappingsToSiteDir( ScalaUnidoc / packageDoc / mappings, docsMappingsAPIDir ), ghpagesNoJekyll := false, - git.remoteRepo := "https://github.com/alonsodomin/cron4s.git", + git.remoteRepo := "https://github.com/alonsodomin/cron4s.git", ScalaUnidoc / unidoc / unidocProjectFilter := inProjects( core.jvm, circe.jvm, @@ -257,7 +263,7 @@ lazy val cron4s = (project in file(".")) lazy val cron4sJS = (project in file(".js")) .settings( - name := "js", + name := "js", moduleName := "cron4s-js" ) .settings(commonSettings: _*) @@ -269,7 +275,7 @@ lazy val cron4sJS = (project in file(".js")) lazy val cron4sJVM = (project in file(".jvm")) .settings( - name := "jvm", + name := "jvm", moduleName := "cron4s-jvm" ) .settings(commonSettings) @@ -296,7 +302,7 @@ lazy val docs = project lazy val core = (crossProject(JSPlatform, JVMPlatform) in file("modules/core")) .enablePlugins(AutomateHeaderPlugin, ScalafmtPlugin, MimaPlugin) .settings( - name := "core", + name := "core", moduleName := "cron4s-core" ) .settings(commonSettings) @@ -313,7 +319,7 @@ lazy val testkit = (crossProject(JSPlatform, JVMPlatform) in file("modules/testkit")) .enablePlugins(AutomateHeaderPlugin, ScalafmtPlugin, MimaPlugin) .settings( - name := "testkit", + name := "testkit", moduleName := "cron4s-testkit" ) .settings(commonSettings) @@ -328,12 +334,11 @@ lazy val testkit = lazy val tests = (crossProject(JSPlatform, JVMPlatform) in file("tests")) .enablePlugins(AutomateHeaderPlugin, ScalafmtPlugin) .settings( - name := "tests", + name := "tests", moduleName := "cron4s-tests" ) .settings(commonSettings) .settings(noPublishSettings) - .settings(Dependencies.tests) .jsSettings(commonJsSettings) .jsSettings(Dependencies.testsJS) .jvmSettings(commonJvmSettings) @@ -343,7 +348,7 @@ lazy val tests = (crossProject(JSPlatform, JVMPlatform) in file("tests")) lazy val bench = (project in file("bench")) .enablePlugins(AutomateHeaderPlugin, ScalafmtPlugin) .settings( - name := "bench", + name := "bench", moduleName := "cron4s-bench" ) .settings(commonSettings) @@ -360,7 +365,7 @@ lazy val bench = (project in file("bench")) lazy val joda = (project in file("modules/joda")) .enablePlugins(AutomateHeaderPlugin, ScalafmtPlugin, MimaPlugin) .settings( - name := "joda", + name := "joda", moduleName := "cron4s-joda", consoleImports ++= Seq("org.joda.time._", "cron4s.lib.joda._") ) @@ -377,7 +382,7 @@ lazy val momentjs = (project in file("modules/momentjs")) .settings(commonJsSettings) .settings(publishSettings) .settings( - name := "momentjs", + name := "momentjs", moduleName := "cron4s-momentjs" ) .settings(Dependencies.momentjs) @@ -391,7 +396,7 @@ lazy val circe = (crossProject(JSPlatform, JVMPlatform).crossType(CrossType.Pure) in file("modules/circe")) .enablePlugins(AutomateHeaderPlugin, ScalafmtPlugin, MimaPlugin) .settings( - name := "circe", + name := "circe", moduleName := "cron4s-circe" ) .settings(commonSettings) @@ -406,7 +411,7 @@ lazy val decline = (crossProject(JSPlatform, JVMPlatform).crossType(CrossType.Pure) in file("modules/decline")) .enablePlugins(AutomateHeaderPlugin, ScalafmtPlugin, MimaPlugin) .settings( - name := "decline", + name := "decline", moduleName := "cron4s-decline" ) .settings(commonSettings) @@ -420,7 +425,7 @@ lazy val decline = lazy val doobie = (project in file("modules/doobie")) .enablePlugins(AutomateHeaderPlugin, ScalafmtPlugin, MimaPlugin) .settings( - name := "doobie", + name := "doobie", moduleName := "cron4s-doobie" ) .settings(commonSettings) diff --git a/modules/core/js/src/main/scala/cron4s/lib/js/JsDateInstance.scala b/modules/core/js/src/main/scala/cron4s/lib/js/JsDateInstance.scala index 859d5ec5..8b8e89b9 100644 --- a/modules/core/js/src/main/scala/cron4s/lib/js/JsDateInstance.scala +++ b/modules/core/js/src/main/scala/cron4s/lib/js/JsDateInstance.scala @@ -16,8 +16,6 @@ package cron4s.lib.js -import cats.syntax.either._ - import cron4s.CronField import cron4s.datetime.{DateTimeError, DateTimeUnit, InvalidFieldValue, IsDateTime} diff --git a/modules/core/shared/src/main/scala/cron4s/datetime/PredicateReducer.scala b/modules/core/shared/src/main/scala-2/cron4s/datetime/PredicateReducer.scala similarity index 100% rename from modules/core/shared/src/main/scala/cron4s/datetime/PredicateReducer.scala rename to modules/core/shared/src/main/scala-2/cron4s/datetime/PredicateReducer.scala diff --git a/modules/core/shared/src/main/scala/cron4s/datetime/Stepper.scala b/modules/core/shared/src/main/scala-2/cron4s/datetime/Stepper.scala similarity index 100% rename from modules/core/shared/src/main/scala/cron4s/datetime/Stepper.scala rename to modules/core/shared/src/main/scala-2/cron4s/datetime/Stepper.scala diff --git a/modules/core/shared/src/main/scala/cron4s/datetime/package.scala b/modules/core/shared/src/main/scala-2/cron4s/datetime/package.scala similarity index 79% rename from modules/core/shared/src/main/scala/cron4s/datetime/package.scala rename to modules/core/shared/src/main/scala-2/cron4s/datetime/package.scala index 0cf80162..f92ecf92 100644 --- a/modules/core/shared/src/main/scala/cron4s/datetime/package.scala +++ b/modules/core/shared/src/main/scala-2/cron4s/datetime/package.scala @@ -26,6 +26,16 @@ import scala.language.implicitConversions * Created by alonsodomin on 24/01/2017. */ package object datetime { + import cron4s.expr._ + private[datetime] def applyRange( + expr: DateCronExpr + ): List[IndexedSeq[Int]] = expr.raw.map(ops.range).toList + private[datetime] def applyRange( + expr: TimeCronExpr + ): List[IndexedSeq[Int]] = expr.raw.map(ops.range).toList + private[datetime] def applyRange( + expr: CronExpr + ): List[IndexedSeq[Int]] = expr.raw.map(ops.range).toList import CronField._ private[datetime] type AnyCron = diff --git a/modules/core/shared/src/main/scala/cron4s/expr/CronExpr.scala b/modules/core/shared/src/main/scala-2/cron4s/expr/CronExpr.scala similarity index 75% rename from modules/core/shared/src/main/scala/cron4s/expr/CronExpr.scala rename to modules/core/shared/src/main/scala-2/cron4s/expr/CronExpr.scala index 93840af5..724c2fcb 100644 --- a/modules/core/shared/src/main/scala/cron4s/expr/CronExpr.scala +++ b/modules/core/shared/src/main/scala-2/cron4s/expr/CronExpr.scala @@ -16,9 +16,6 @@ package cron4s.expr -import cats.{Eq, Show} -import cats.implicits._ - import shapeless._ /** @@ -51,17 +48,4 @@ final case class CronExpr( raw.map(_root_.cron4s.expr.ops.show).toList.mkString(" ") } -object CronExpr { - implicit val CronExprEq: Eq[CronExpr] = - Eq.instance { (lhs, rhs) => - lhs.seconds === rhs.seconds && - lhs.minutes === rhs.minutes && - lhs.hours === rhs.hours && - lhs.daysOfMonth === rhs.daysOfMonth && - lhs.months === rhs.months && - lhs.daysOfWeek === rhs.daysOfWeek - } - - implicit val CronExprShow: Show[CronExpr] = - Show.fromToString[CronExpr] -} +object CronExpr extends Cron4sInstances diff --git a/modules/core/shared/src/main/scala/cron4s/expr/FieldSelector.scala b/modules/core/shared/src/main/scala-2/cron4s/expr/FieldSelector.scala similarity index 100% rename from modules/core/shared/src/main/scala/cron4s/expr/FieldSelector.scala rename to modules/core/shared/src/main/scala-2/cron4s/expr/FieldSelector.scala diff --git a/modules/core/shared/src/main/scala/cron4s/expr/NodeConversions.scala b/modules/core/shared/src/main/scala-2/cron4s/expr/NodeConversions.scala similarity index 100% rename from modules/core/shared/src/main/scala/cron4s/expr/NodeConversions.scala rename to modules/core/shared/src/main/scala-2/cron4s/expr/NodeConversions.scala diff --git a/modules/core/shared/src/main/scala/cron4s/expr/wrappers.scala b/modules/core/shared/src/main/scala-2/cron4s/expr/WrapperInstances.scala similarity index 85% rename from modules/core/shared/src/main/scala/cron4s/expr/wrappers.scala rename to modules/core/shared/src/main/scala-2/cron4s/expr/WrapperInstances.scala index 3395a1b7..3920be91 100644 --- a/modules/core/shared/src/main/scala/cron4s/expr/wrappers.scala +++ b/modules/core/shared/src/main/scala-2/cron4s/expr/WrapperInstances.scala @@ -15,28 +15,17 @@ */ package cron4s.expr - import cats.{Eq, Show} +import shapeless._ import cron4s.{CronField, CronUnit} import cron4s.base.Predicate -import shapeless._ - -/** - * Created by alonsodomin on 23/01/2017. - */ -final class FieldNode[F <: CronField](private[cron4s] val raw: RawFieldNode[F]) extends AnyVal { - override def toString: String = raw.fold(_root_.cron4s.expr.ops.show) -} - -object FieldNode { +private[cron4s] trait FieldNodeInstances { implicit def fieldNodeEq[F <: CronField]: Eq[FieldNode[F]] = Eq.fromUniversalEquals - implicit def fieldNodeShow[F <: CronField]: Show[FieldNode[F]] = Show.fromToString[FieldNode[F]] - implicit def fieldNodeInstance[F <: CronField]: FieldExpr[FieldNode, F] = new FieldExpr[FieldNode, F] { def matches(node: FieldNode[F]): Predicate[Int] = @@ -56,18 +45,12 @@ object FieldNode { case Inr(Inr(Inr(Inr(Inl(every))))) => every.implies(ee) case _ => sys.error("Impossible!") } - def unit(node: FieldNode[F]): CronUnit[F] = node.raw.fold(_root_.cron4s.expr.ops.unit) } } -final class FieldNodeWithAny[F <: CronField](private[cron4s] val raw: RawFieldNodeWithAny[F]) - extends AnyVal { - override def toString: String = raw.fold(_root_.cron4s.expr.ops.show) -} - -object FieldNodeWithAny { +private[cron4s] trait FieldNodeWithAnyInstances { implicit def fieldNodeWithAnyEq[F <: CronField]: Eq[FieldNodeWithAny[F]] = Eq.fromUniversalEquals @@ -81,7 +64,6 @@ object FieldNodeWithAny { def range(node: FieldNodeWithAny[F]): IndexedSeq[Int] = node.raw.fold(_root_.cron4s.expr.ops.range) - def implies[EE[_ <: CronField]]( node: FieldNodeWithAny[F] )(ee: EE[F])(implicit EE: FieldExpr[EE, F]): Boolean = @@ -95,12 +77,7 @@ object FieldNodeWithAny { } } -final class EnumerableNode[F <: CronField](private[cron4s] val raw: RawEnumerableNode[F]) - extends AnyVal { - override def toString: String = raw.fold(_root_.cron4s.expr.ops.show) -} - -object EnumerableNode { +private[cron4s] trait EnumerableNodeInstances { implicit def enumerableNodeEq[F <: CronField]: Eq[EnumerableNode[F]] = Eq.fromUniversalEquals @@ -120,7 +97,6 @@ object EnumerableNode { case Inr(Inl(between)) => between.implies(ee) case _ => sys.error("Impossible!") } - def range(node: EnumerableNode[F]): IndexedSeq[Int] = node.raw.fold(_root_.cron4s.expr.ops.range) @@ -129,12 +105,7 @@ object EnumerableNode { } } -final class DivisibleNode[F <: CronField](private[cron4s] val raw: RawDivisibleNode[F]) - extends AnyVal { - override def toString: String = raw.fold(_root_.cron4s.expr.ops.show) -} - -object DivisibleNode { +private[cron4s] trait DivisibleNodeInstances { implicit def divisibleNodeEq[F <: CronField]: Eq[DivisibleNode[F]] = Eq.fromUniversalEquals diff --git a/modules/core/shared/src/main/scala/cron4s/expr/ops.scala b/modules/core/shared/src/main/scala-2/cron4s/expr/ops.scala similarity index 99% rename from modules/core/shared/src/main/scala/cron4s/expr/ops.scala rename to modules/core/shared/src/main/scala-2/cron4s/expr/ops.scala index 77fe2800..aaf05c37 100644 --- a/modules/core/shared/src/main/scala/cron4s/expr/ops.scala +++ b/modules/core/shared/src/main/scala-2/cron4s/expr/ops.scala @@ -23,6 +23,7 @@ import cron4s.CronField import shapeless._ import cron4s.base.Predicate import cron4s.CronUnit + /** * Created by alonsodomin on 17/12/2016. */ diff --git a/modules/core/shared/src/main/scala/cron4s/expr/package.scala b/modules/core/shared/src/main/scala-2/cron4s/expr/package.scala similarity index 100% rename from modules/core/shared/src/main/scala/cron4s/expr/package.scala rename to modules/core/shared/src/main/scala-2/cron4s/expr/package.scala diff --git a/modules/core/shared/src/main/scala/cron4s/expr/parts.scala b/modules/core/shared/src/main/scala-2/cron4s/expr/parts.scala similarity index 68% rename from modules/core/shared/src/main/scala/cron4s/expr/parts.scala rename to modules/core/shared/src/main/scala-2/cron4s/expr/parts.scala index 6b76eab2..9758baac 100644 --- a/modules/core/shared/src/main/scala/cron4s/expr/parts.scala +++ b/modules/core/shared/src/main/scala-2/cron4s/expr/parts.scala @@ -32,16 +32,7 @@ final case class DateCronExpr( raw.map(_root_.cron4s.expr.ops.show).toList.mkString(" ") } -object DateCronExpr { - implicit val dateCronEq: Eq[DateCronExpr] = Eq.instance { (lhs, rhs) => - lhs.daysOfMonth === rhs.daysOfMonth && - lhs.months === rhs.months && - lhs.daysOfWeek === rhs.daysOfWeek - } - - implicit val dateCronShow: Show[DateCronExpr] = - Show.fromToString[DateCronExpr] -} +object DateCronExpr extends DateCronExprInstances final case class TimeCronExpr( seconds: SecondsNode, @@ -54,13 +45,4 @@ final case class TimeCronExpr( raw.map(_root_.cron4s.expr.ops.show).toList.mkString(" ") } -object TimeCronExpr { - implicit val timeCronEq: Eq[TimeCronExpr] = Eq.instance { (lhs, rhs) => - lhs.seconds === rhs.seconds && - lhs.minutes === rhs.minutes && - lhs.hours === rhs.hours - } - - implicit val timeCronShow: Show[TimeCronExpr] = - Show.fromToString[TimeCronExpr] -} +object TimeCronExpr extends TimeCronExprInstances diff --git a/modules/core/shared/src/main/scala-2/cron4s/expr/wrappers.scala b/modules/core/shared/src/main/scala-2/cron4s/expr/wrappers.scala new file mode 100644 index 00000000..ffcc0b75 --- /dev/null +++ b/modules/core/shared/src/main/scala-2/cron4s/expr/wrappers.scala @@ -0,0 +1,54 @@ +/* + * Copyright 2017 Antonio Alonso Dominguez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cron4s.expr + +import cats.{Eq, Show} + +import cron4s.{CronField, CronUnit} +import cron4s.base.Predicate + +import shapeless._ + +/** + * Created by alonsodomin on 23/01/2017. + */ +final class FieldNode[F <: CronField](private[cron4s] val raw: RawFieldNode[F]) extends AnyVal { + override def toString: String = raw.fold(_root_.cron4s.expr.ops.show) +} + +object FieldNode extends FieldNodeInstances + +final class FieldNodeWithAny[F <: CronField](private[cron4s] val raw: RawFieldNodeWithAny[F]) + extends AnyVal { + override def toString: String = raw.fold(_root_.cron4s.expr.ops.show) +} + +object FieldNodeWithAny extends FieldNodeWithAnyInstances + +final class EnumerableNode[F <: CronField](private[cron4s] val raw: RawEnumerableNode[F]) + extends AnyVal { + override def toString: String = raw.fold(_root_.cron4s.expr.ops.show) +} + +object EnumerableNode extends EnumerableNodeInstances + +final class DivisibleNode[F <: CronField](private[cron4s] val raw: RawDivisibleNode[F]) + extends AnyVal { + override def toString: String = raw.fold(_root_.cron4s.expr.ops.show) +} + +object DivisibleNode extends DivisibleNodeInstances diff --git a/modules/core/shared/src/main/scala/cron4s/validation/NodeValidator.scala b/modules/core/shared/src/main/scala-2/cron4s/validation/NodeValidator.scala similarity index 98% rename from modules/core/shared/src/main/scala/cron4s/validation/NodeValidator.scala rename to modules/core/shared/src/main/scala-2/cron4s/validation/NodeValidator.scala index db02d91a..eac8b75b 100644 --- a/modules/core/shared/src/main/scala/cron4s/validation/NodeValidator.scala +++ b/modules/core/shared/src/main/scala-2/cron4s/validation/NodeValidator.scala @@ -135,7 +135,7 @@ private[validation] trait NodeValidatorInstances extends LowPriorityNodeValidato new NodeValidator[EveryNode[F]] { def validate(node: EveryNode[F]): List[InvalidField] = { lazy val baseErrors = NodeValidator[DivisibleNode[F]].validate(node.base) - val evenlyDivided = (node.base.range.size % node.freq) == 0 + val evenlyDivided = (toEnumeratedOps(node.base).range.size % node.freq) == 0 if (!evenlyDivided) InvalidField( node.unit.field, diff --git a/modules/core/shared/src/main/scala/cron4s/validation/ops.scala b/modules/core/shared/src/main/scala-2/cron4s/validation/ops.scala similarity index 100% rename from modules/core/shared/src/main/scala/cron4s/validation/ops.scala rename to modules/core/shared/src/main/scala-2/cron4s/validation/ops.scala diff --git a/modules/core/shared/src/main/scala/cron4s/validation/package.scala b/modules/core/shared/src/main/scala-2/cron4s/validation/package.scala similarity index 100% rename from modules/core/shared/src/main/scala/cron4s/validation/package.scala rename to modules/core/shared/src/main/scala-2/cron4s/validation/package.scala diff --git a/modules/core/shared/src/main/scala-3/cron4s/datetime/PredicateReducer.scala b/modules/core/shared/src/main/scala-3/cron4s/datetime/PredicateReducer.scala new file mode 100644 index 00000000..3669dd10 --- /dev/null +++ b/modules/core/shared/src/main/scala-3/cron4s/datetime/PredicateReducer.scala @@ -0,0 +1,74 @@ +/* + * Copyright 2017 Antonio Alonso Dominguez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cron4s.datetime + +import cats.MonoidK +import cats.instances.list._ +import cron4s.CronField +import cron4s.base._ +import cron4s.expr._ +import cron4s.syntax.predicate._ + +/** + * Created by domingueza on 29/07/2016. + */ +private[datetime] final class PredicateReducer[DateTime](DT: IsDateTime[DateTime])(implicit + M: MonoidK[Predicate] +) { + private def predicateFor[N[_ <: CronField], F <: CronField](field: F, node: N[F])(implicit + expr: FieldExpr[N, F] + ): Predicate[DateTime] = + Predicate { dt => + DT.get(dt, field) + .map(expr.matches(node)) + .getOrElse(M.empty[DateTime](dt)) + } + type Predicatable = + SecondsNode | MinutesNode | HoursNode | DaysOfMonthNode | MonthsNode | DaysOfWeekNode + + type FromRawable = CronExpr | DateCronExpr | TimeCronExpr + import CronField._ + def fromRaw(t: FromRawable): List[Predicate[DateTime]] = t match { + case t: CronExpr => t.raw match + case (seconds, minutes, hours, daysOfMonth, months, daysOfWeek) => + List( + predicateFor(Second, seconds), + predicateFor(Minute, minutes), + predicateFor(Hour, hours), + predicateFor(DayOfMonth, daysOfMonth), + predicateFor(Month, months), + predicateFor(DayOfWeek, daysOfWeek), + ) + case t: DateCronExpr => t.raw match + case (daysOfMonth, months, daysOfWeek) => + List( + predicateFor(DayOfMonth, daysOfMonth), + predicateFor(Month, months), + predicateFor(DayOfWeek, daysOfWeek), + ) + case t: TimeCronExpr => t.raw match + case (seconds, minutes, hours) => + List( + predicateFor(Second, seconds), + predicateFor(Minute, minutes), + predicateFor(Hour, hours), + ) + } + + def run(cron: AnyCron): Predicate[DateTime] = + asOf(fromRaw(cron)) +} diff --git a/modules/core/shared/src/main/scala-3/cron4s/datetime/Stepper.scala b/modules/core/shared/src/main/scala-3/cron4s/datetime/Stepper.scala new file mode 100644 index 00000000..258b8e80 --- /dev/null +++ b/modules/core/shared/src/main/scala-3/cron4s/datetime/Stepper.scala @@ -0,0 +1,149 @@ +/* + * Copyright 2017 Antonio Alonso Dominguez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cron4s.datetime + +import cats.syntax.either._ + +import cron4s._ +import cron4s.base.{Direction, Step} +import cron4s.expr._ + +import scala.annotation.tailrec + +private[datetime] final class Stepper[DateTime](DT: IsDateTime[DateTime]) { + private type ResetPrevFn = DateTime => Option[DateTime] + private type StepST = Option[(ResetPrevFn, DateTime, Step)] + + private val identityReset: ResetPrevFn = Some(_) + + private[this] def stepNode[N[_ <: CronField], F <: CronField](stepState: StepST, node: N[F])( + implicit expr: FieldExpr[N, F] + ): StepST = { + def attemptSet( + dt: DateTime, + step: Step, + newValue: Int, + carryOver: Int + ): Option[(DateTime, Int)] = + DT.set(dt, node.unit.field, newValue) + .map(_ -> carryOver) + .recover { + case InvalidFieldValue(_, _) => + val newCarryOver = step.direction match { + case Direction.Forward => Math.max(carryOver, step.direction.sign) + case Direction.Backwards => + Math.min(carryOver, step.direction.sign) + } + dt -> newCarryOver + } + .toOption + + stepState.flatMap { + case (resetPrevious, from, step) => + def resetThis: DateTime => Option[DateTime] = { + val resetValue = step.direction match { + case Direction.Forward => node.min + case Direction.Backwards => node.max + } + + resetPrevious.andThen(_.flatMap(DT.set(_, node.unit.field, resetValue).toOption)) + } + + DT.get(from, node.unit.field).toOption.flatMap { currentValue => + node.step(currentValue, step) match { + case Some((newValue, carryOver)) => + // Attempt to set a new value in the field and reset previous fields + attemptSet(from, step, newValue, carryOver) + .flatMap { case (dt, co) => resetPrevious(dt).map(_ -> co) } + .map { + case (dt, co) => + (resetThis, dt, step.copy(amount = Math.abs(co))) + } + + case None => + Some((resetThis, from, step.copy(amount = 0))) + } + } + } + } + + private[this] def stepOverMonth(prev: StepST, expr: MonthsNode): StepST = + for { + (_, dt, s @ Step(carryOver, dir)) <- stepNode(prev, expr) + newDateTime <- DT.plus(dt, carryOver * 12 * dir.sign, DateTimeUnit.Months) + } yield (identityReset, newDateTime, s.copy(amount = 0)) + + private[this] def stepOverDayOfWeek(prev: StepST, expr: DaysOfWeekNode): StepST = + for { + (_, dt, s @ Step(carryOver, dir)) <- stepNode(prev, expr) + newDateTime <- DT.plus(dt, carryOver * dir.sign, DateTimeUnit.Weeks) + } yield (identityReset, newDateTime, s.copy(amount = 0)) + + + type FoldInternalExprable = CronExpr | DateCronExpr | TimeCronExpr + def foldInternalExpr( + stepSt: StepST, + expr: FoldInternalExprable + ): Option[(ResetPrevFn, DateTime, Step)] = expr match { + case expr: CronExpr => + val (dom,mt) = expr.datePart.raw.take(2) + val (_, _, daysOfWeekNode) = expr.datePart.raw + + for { + st @ (resetTime, _, _) <- foldInternalExpr(stepSt,expr.timePart) + (_, dt, step) <- + List( + (step:StepST) => stepNode(step,dom), + (step:StepST) => stepOverMonth(step,mt), + ).foldLeft(Some(st): StepST){case (step,f) => f(step)} + result <- stepOverDayOfWeek(Some((resetTime, dt, step)), daysOfWeekNode) + } yield result + case expr: DateCronExpr => + expr.raw match + case (daysOfMonth,month,daysOfWeek) => + List( + (step:StepST) => stepNode(step, daysOfMonth), + (step:StepST) => stepOverMonth(step,month), + (step:StepST) => stepOverDayOfWeek(step,daysOfWeek), + ).foldLeft(stepSt){case (step,f) => f(step)} + case expr: TimeCronExpr => + expr.raw match + case (seconds, minutes, hours) => + List( + (step:StepST) => stepNode(step, seconds), + (step:StepST) => stepNode(step, minutes), + (step:StepST) => stepNode(step, hours), + ).foldLeft(stepSt){case (step,f) => f(step)} + } + + def run(cron: AnyCron, from: DateTime, step: Step): Option[DateTime] = { + def initial(dt: DateTime): StepST = + Some((identityReset, dt, step.copy(amount = 1))) + + @tailrec + def go(stepSt: StepST, iteration: Int): StepST = + if (iteration == step.amount) stepSt + else + foldInternalExpr(stepSt, cron) match { + case Some((_, dt, _)) => + go(initial(dt), iteration + 1) + case None => None + } + + go(initial(from), 0).map(_._2) + } +} diff --git a/modules/core/shared/src/main/scala-3/cron4s/datetime/package.scala b/modules/core/shared/src/main/scala-3/cron4s/datetime/package.scala new file mode 100644 index 00000000..c893e8f8 --- /dev/null +++ b/modules/core/shared/src/main/scala-3/cron4s/datetime/package.scala @@ -0,0 +1,76 @@ +/* + * Copyright 2017 Antonio Alonso Dominguez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cron4s + +import cron4s.expr.{DateCronExpr, TimeCronExpr} + +import scala.language.implicitConversions + +/** + * Created by alonsodomin on 24/01/2017. + */ +package object datetime { + private[datetime] def applyRange( + expr: DateCronExpr + ): List[IndexedSeq[Int]] = { + expr.raw match { + case (daysOfMonth, months, daysOfWeek) => + List( + cron4s.expr.ops.range(daysOfMonth), + cron4s.expr.ops.range(months), + cron4s.expr.ops.range(daysOfWeek) + ) + } + } + private[datetime] def applyRange( + expr: TimeCronExpr + ): List[IndexedSeq[Int]] = { + expr.raw match { + case (seconds, minutes, hours) => + List( + cron4s.expr.ops.range(seconds), + cron4s.expr.ops.range(minutes), + cron4s.expr.ops.range(hours) + ) + } + } + private[datetime] def applyRange( + expr: CronExpr + ): List[IndexedSeq[Int]] = { + expr.raw match { + case (seconds, minutes, hours, daysOfMonth, months, daysOfWeek) => + List( + cron4s.expr.ops.range(seconds), + cron4s.expr.ops.range(minutes), + cron4s.expr.ops.range(hours), + cron4s.expr.ops.range(daysOfMonth), + cron4s.expr.ops.range(months), + cron4s.expr.ops.range(daysOfWeek) + ) + } + } + + import CronField._ + + private[datetime] type AnyCron = + CronExpr | TimeCronExpr | DateCronExpr + + private[datetime] type FieldSeq = + Second *: Minute *: Hour *: DayOfMonth *: Month *: DayOfWeek *: EmptyTuple + val FieldSeq: FieldSeq = + Second *: Minute *: Hour *: DayOfMonth *: Month *: DayOfWeek *: EmptyTuple +} diff --git a/modules/core/shared/src/main/scala-3/cron4s/expr/CronExpr.scala b/modules/core/shared/src/main/scala-3/cron4s/expr/CronExpr.scala new file mode 100644 index 00000000..ffe3a749 --- /dev/null +++ b/modules/core/shared/src/main/scala-3/cron4s/expr/CronExpr.scala @@ -0,0 +1,61 @@ +/* + * Copyright 2017 Antonio Alonso Dominguez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cron4s.expr +import cats.syntax.all._ + +/** + * Representation of a valid CRON expression as an AST + * + * @author Antonio Alonso Dominguez + */ +final case class CronExpr( + seconds: SecondsNode, + minutes: MinutesNode, + hours: HoursNode, + daysOfMonth: DaysOfMonthNode, + months: MonthsNode, + daysOfWeek: DaysOfWeekNode +) { + private[cron4s] lazy val raw: RawCronExpr = + (seconds, minutes, hours, daysOfMonth, months, daysOfWeek) + + /** + * Time part of the CRON expression + */ + lazy val timePart: TimeCronExpr = TimeCronExpr(seconds, minutes, hours) + + /** + * Date part of the CRON expression + */ + lazy val datePart: DateCronExpr = + DateCronExpr(daysOfMonth, months, daysOfWeek) + + override lazy val toString: String = + raw match { + case (sec, min, hs, d, m, dw) => + List( + _root_.cron4s.expr.ops.show(sec), + _root_.cron4s.expr.ops.show(min), + _root_.cron4s.expr.ops.show(hs), + _root_.cron4s.expr.ops.show(d), + _root_.cron4s.expr.ops.show(m), + _root_.cron4s.expr.ops.show(dw) + ).mkString(" ") + } +} + +object CronExpr extends Cron4sInstances diff --git a/modules/core/shared/src/main/scala-3/cron4s/expr/FieldSelector.scala b/modules/core/shared/src/main/scala-3/cron4s/expr/FieldSelector.scala new file mode 100644 index 00000000..4c565c33 --- /dev/null +++ b/modules/core/shared/src/main/scala-3/cron4s/expr/FieldSelector.scala @@ -0,0 +1,137 @@ +/* + * Copyright 2017 Antonio Alonso Dominguez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cron4s.expr + +import cron4s.CronField + + +import scala.annotation.implicitNotFound + +/** + * Created by alonsodomin on 10/02/2017. + */ +@implicitNotFound("Field ${F} is not a member of expression ${A}") +sealed trait FieldSelector[A, F <: CronField] { + type Raw <: Tuple + type Out[X <: CronField] + + val hlistSelect: (expr:Raw) => Out[F] + def selectFrom(expr: A): Out[F] +} + +object FieldSelector { + import CronField._ + + def apply[A, F <: CronField](implicit ev: FieldSelector[A, F]): FieldSelector[A, F] = ev + + implicit val SecondsFromCronExpr: FieldSelector[CronExpr, Second] = + new FullCronFieldNodeSelector[Second] { + val hlistSelect = (expr:RawCronExpr) => expr._1 + } + implicit val SecondsFromTimeExpr: FieldSelector[TimeCronExpr, Second] = + new TimeCronFieldNodeSelector[Second] { + val hlistSelect = (expr:RawTimeCronExpr) => expr._1 + } + + implicit val MinutesFromCronExpr: FieldSelector[CronExpr, Minute] = + new FullCronFieldNodeSelector[Minute] { + val hlistSelect = (expr:RawCronExpr) => expr._2 + } + implicit val MinutesFromTimeExpr: FieldSelector[TimeCronExpr, Minute] = + new TimeCronFieldNodeSelector[Minute] { + val hlistSelect = (expr:RawTimeCronExpr) => expr._2 + + } + + implicit val HoursFromCronExpr: FieldSelector[CronExpr, Hour] = + new FullCronFieldNodeSelector[Hour] { + val hlistSelect = (expr:RawCronExpr) => expr._3 + } + implicit val HoursFromTimeExpr: FieldSelector[TimeCronExpr, Hour] = + new TimeCronFieldNodeSelector[Hour] { + val hlistSelect = (expr:RawTimeCronExpr) => expr._3 + } + + implicit val DayOfMonthFromCronExpr: FieldSelector[CronExpr, DayOfMonth] = + new FullCronFieldNodeWithAnySelector[DayOfMonth] { + val hlistSelect = (expr:RawCronExpr) => expr._4 + } + implicit val DayOfMonthFromDateExpr: FieldSelector[DateCronExpr, DayOfMonth] = + new DateCronFieldNodeWithAnySelector[DayOfMonth] { + val hlistSelect = (expr:RawDateCronExpr) => expr._1 + } + + implicit val MonthFromCronExpr: FieldSelector[CronExpr, Month] = + new FullCronFieldNodeSelector[Month] { + val hlistSelect = (expr:RawCronExpr) => expr._5 + } + implicit val MonthFromDateExpr: FieldSelector[DateCronExpr, Month] = + new DateCronFieldNodeSelector[Month] { + val hlistSelect = (expr:RawDateCronExpr) => expr._2 + } + + implicit val DayOfWeekFromCronExpr: FieldSelector[CronExpr, DayOfWeek] = + new FullCronFieldNodeWithAnySelector[DayOfWeek] { + val hlistSelect = (expr:RawCronExpr) => expr._6 + } + implicit val DayOfWeekFromDateExpr: FieldSelector[DateCronExpr, DayOfWeek] = + new DateCronFieldNodeWithAnySelector[DayOfWeek] { + val hlistSelect = (expr:RawDateCronExpr) => expr._3 + } + + // Base classes adding type refinements for the typeclass instances + + private[this] abstract class FieldNodeSelector[A, F <: CronField] extends FieldSelector[A, F] { + type Out[X <: CronField] = FieldNode[X] + } + private[this] abstract class FullCronFieldNodeSelector[F <: CronField] + extends FieldNodeSelector[CronExpr, F] { + type Raw = RawCronExpr + + def selectFrom(expr: CronExpr): FieldNode[F] = hlistSelect(expr.raw) + } + private[this] abstract class TimeCronFieldNodeSelector[F <: CronField] + extends FieldNodeSelector[TimeCronExpr, F] { + type Raw = RawTimeCronExpr + + def selectFrom(expr: TimeCronExpr): FieldNode[F] = hlistSelect(expr.raw) + } + private[this] abstract class DateCronFieldNodeSelector[F <: CronField] + extends FieldNodeSelector[DateCronExpr, F] { + type Raw = RawDateCronExpr + + def selectFrom(expr: DateCronExpr): FieldNode[F] = hlistSelect(expr.raw) + + } + + private[this] abstract class FieldNodeWithAnySelector[A, F <: CronField] + extends FieldSelector[A, F] { + type Out[X <: CronField] = FieldNodeWithAny[X] + } + private[this] abstract class FullCronFieldNodeWithAnySelector[F <: CronField] + extends FieldNodeWithAnySelector[CronExpr, F] { + type Raw = RawCronExpr + + def selectFrom(expr: CronExpr): FieldNodeWithAny[F] = hlistSelect(expr.raw) + } + private[this] abstract class DateCronFieldNodeWithAnySelector[F <: CronField] + extends FieldNodeWithAnySelector[DateCronExpr, F] { + type Raw = RawDateCronExpr + + def selectFrom(expr: DateCronExpr): FieldNodeWithAny[F] = hlistSelect(expr.raw) + } +} diff --git a/modules/core/shared/src/main/scala-3/cron4s/expr/NodeConversions.scala b/modules/core/shared/src/main/scala-3/cron4s/expr/NodeConversions.scala new file mode 100644 index 00000000..0e719871 --- /dev/null +++ b/modules/core/shared/src/main/scala-3/cron4s/expr/NodeConversions.scala @@ -0,0 +1,83 @@ +/* + * Copyright 2017 Antonio Alonso Dominguez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cron4s.expr + +import cron4s.CronField + +import scala.language.implicitConversions + +/** + * Created by alonsodomin on 28/12/2016. + */ +private[cron4s] trait NodeConversions { + implicit def each2Field[F <: CronField](node: EachNode[F]): FieldNode[F] = + new FieldNode(node) + + implicit def const2Field[F <: CronField](node: ConstNode[F]): FieldNode[F] = + new FieldNode(node) + + implicit def between2Field[F <: CronField](node: BetweenNode[F]): FieldNode[F] = + new FieldNode(node) + + implicit def several2Field[F <: CronField](node: SeveralNode[F]): FieldNode[F] = + new FieldNode(node) + + implicit def every2Field[F <: CronField](node: EveryNode[F]): FieldNode[F] = + new FieldNode(node) + + implicit def any2FieldWithAny[F <: CronField](node: AnyNode[F]): FieldNodeWithAny[F] = + new FieldNodeWithAny(node) + + implicit def each2FieldWithAny[F <: CronField](node: EachNode[F]): FieldNodeWithAny[F] = + new FieldNodeWithAny(node) + + implicit def const2FieldWithAny[F <: CronField](node: ConstNode[F]): FieldNodeWithAny[F] = + new FieldNodeWithAny(node) + + implicit def between2FieldWithAny[F <: CronField](node: BetweenNode[F]): FieldNodeWithAny[F] = + new FieldNodeWithAny(node) + + implicit def several2FieldWithAny[F <: CronField](node: SeveralNode[F]): FieldNodeWithAny[F] = + new FieldNodeWithAny(node) + + implicit def every2FieldWithAny[F <: CronField](node: EveryNode[F]): FieldNodeWithAny[F] = + new FieldNodeWithAny(node) + + implicit def field2FieldWithAny[F <: CronField](node: FieldNode[F]): FieldNodeWithAny[F] = + new FieldNodeWithAny[F](node.raw) + + implicit def const2Enumerable[F <: CronField](node: ConstNode[F]): EnumerableNode[F] = + new EnumerableNode(node) + + implicit def between2Enumerable[F <: CronField](node: BetweenNode[F]): EnumerableNode[F] = + new EnumerableNode(node) + + implicit def each2Divisible[F <: CronField](node: EachNode[F]): DivisibleNode[F] = + new DivisibleNode(node) + + implicit def between2Divisible[F <: CronField](node: BetweenNode[F]): DivisibleNode[F] = + new DivisibleNode(node) + + implicit def several2Divisible[F <: CronField](node: SeveralNode[F]): DivisibleNode[F] = + new DivisibleNode(node) + + implicit def enumerable2Field[F <: CronField](node: EnumerableNode[F]): FieldNode[F] = + new FieldNode[F](node.raw) + + implicit def divisible2Field[F <: CronField](node: DivisibleNode[F]): FieldNode[F] = + new FieldNode[F](node.raw) +} diff --git a/modules/core/shared/src/main/scala-3/cron4s/expr/WrapperInstances.scala b/modules/core/shared/src/main/scala-3/cron4s/expr/WrapperInstances.scala new file mode 100644 index 00000000..bd97ca42 --- /dev/null +++ b/modules/core/shared/src/main/scala-3/cron4s/expr/WrapperInstances.scala @@ -0,0 +1,145 @@ +/* + * Copyright 2017 Antonio Alonso Dominguez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cron4s.expr +import cats.{Eq, Show} +import scala.language.implicitConversions +import cron4s.toExprOps +import cron4s.{CronField, CronUnit} +import cron4s.base.Predicate + +private[cron4s] trait FieldNodeInstances { + implicit def fieldNodeEq[F <: CronField]: Eq[FieldNode[F]] = + Eq.fromUniversalEquals + implicit def fieldNodeShow[F <: CronField]: Show[FieldNode[F]] = + Show.fromToString[FieldNode[F]] + implicit def fieldNodeInstance[F <: CronField]: FieldExpr[FieldNode, F] = + new FieldExpr[FieldNode, F] { + def matches(node: FieldNode[F]): Predicate[Int] = + _root_.cron4s.expr.ops.matches(node.raw) + + def range(node: FieldNode[F]): IndexedSeq[Int] = + _root_.cron4s.expr.ops.range(node.raw) + + def implies[EE[_ <: CronField]]( + node: FieldNode[F] + )(ee: EE[F])(implicit EE: FieldExpr[EE, F]): Boolean = + node.raw match { + case each: EachNode[F] => each.implies(ee) + case const: ConstNode[F] => const.implies(ee) + case between: BetweenNode[F] => between.implies(ee) + case several: SeveralNode[F] => several.implies(ee) + case every: EveryNode[F] => every.implies(ee) + case null => + sys.error("expect RawFieldNode[F] but got null at `FieldExpr[FieldNode,F]#implies`") + } + + def unit(node: FieldNode[F]): CronUnit[F] = + _root_.cron4s.expr.ops.unit(node.raw) + } +} + +private[cron4s] trait FieldNodeWithAnyInstances { + implicit def fieldNodeWithAnyEq[F <: CronField]: Eq[FieldNodeWithAny[F]] = + Eq.fromUniversalEquals + + implicit def fieldNodeWithAnyShow[F <: CronField]: Show[FieldNodeWithAny[F]] = + Show.fromToString[FieldNodeWithAny[F]] + + implicit def fieldNodeWithAnyInstance[F <: CronField]: FieldExpr[FieldNodeWithAny, F] = + new FieldExpr[FieldNodeWithAny, F] { + def matches(node: FieldNodeWithAny[F]): Predicate[Int] = + _root_.cron4s.expr.ops.matches(node.raw) + + def range(node: FieldNodeWithAny[F]): IndexedSeq[Int] = + _root_.cron4s.expr.ops.range(node.raw) + + def implies[EE[_ <: CronField]]( + node: FieldNodeWithAny[F] + )(ee: EE[F])(implicit EE: FieldExpr[EE, F]): Boolean = + node.raw match { + case any: AnyNode[F] => any.implies(ee) + case tail: RawFieldNode[F] => new FieldNode[F](tail).implies(ee) + } + + def unit(node: FieldNodeWithAny[F]): CronUnit[F] = + _root_.cron4s.expr.ops.unit(node.raw) + } +} + +private[cron4s] trait EnumerableNodeInstances { + implicit def enumerableNodeEq[F <: CronField]: Eq[EnumerableNode[F]] = + Eq.fromUniversalEquals + + implicit def enumerableNodeShow[F <: CronField]: Show[EnumerableNode[F]] = + Show.fromToString[EnumerableNode[F]] + + implicit def enumerableNodeInstance[F <: CronField]: FieldExpr[EnumerableNode, F] = + new FieldExpr[EnumerableNode, F] { + def matches(node: EnumerableNode[F]): Predicate[Int] = + _root_.cron4s.expr.ops.matches(node.raw) + + def implies[EE[_ <: CronField]]( + node: EnumerableNode[F] + )(ee: EE[F])(implicit EE: FieldExpr[EE, F]): Boolean = + node.raw match { + case const: ConstNode[F] => const.implies(ee) + case between: BetweenNode[F] => between.implies(ee) + case null => + sys.error( + "expect RawEnumerableNode[F] but got null at `FieldExpr[EnumerableNode,F]#implies`" + ) + } + + def range(node: EnumerableNode[F]): IndexedSeq[Int] = + _root_.cron4s.expr.ops.range(node.raw) + + def unit(node: EnumerableNode[F]): CronUnit[F] = + _root_.cron4s.expr.ops.unit(node.raw) + } +} + +private[cron4s] trait DivisibleNodeInstances { + implicit def divisibleNodeEq[F <: CronField]: Eq[DivisibleNode[F]] = + Eq.fromUniversalEquals + + implicit def divisibleNodeShow[F <: CronField]: Show[DivisibleNode[F]] = + Show.fromToString[DivisibleNode[F]] + + implicit def divisibleNodeInstance[F <: CronField]: FieldExpr[DivisibleNode, F] = + new FieldExpr[DivisibleNode, F] { + def matches(node: DivisibleNode[F]): Predicate[Int] = + _root_.cron4s.expr.ops.matches(node.raw) + + def implies[EE[_ <: CronField]]( + node: DivisibleNode[F] + )(ee: EE[F])(implicit EE: FieldExpr[EE, F]): Boolean = + node.raw match { + case each: EachNode[F] => each.implies(ee) + case between: BetweenNode[F] => between.implies(ee) + case several: SeveralNode[F] => several.implies(ee) + case null => + sys.error( + "expect DivisibleNode[F] but got null at `FieldExpr[DivisibleNode,F]#implies`" + ) + } + + def range(node: DivisibleNode[F]): IndexedSeq[Int] = + _root_.cron4s.expr.ops.range(node.raw) + + def unit(node: DivisibleNode[F]): CronUnit[F] = _root_.cron4s.expr.ops.unit(node.raw) + } +} diff --git a/modules/core/shared/src/main/scala-3/cron4s/expr/ops.scala b/modules/core/shared/src/main/scala-3/cron4s/expr/ops.scala new file mode 100644 index 00000000..d01e487a --- /dev/null +++ b/modules/core/shared/src/main/scala-3/cron4s/expr/ops.scala @@ -0,0 +1,81 @@ +/* + * Copyright 2017 Antonio Alonso Dominguez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cron4s.expr + +import cron4s.CronField +import cats.syntax.all.toShow +import cron4s.base.Predicate +import cron4s.CronUnit + +/** + * Created by alonsodomin on 17/12/2016. + */ +private[cron4s] object ops { + type PolyField[F <: CronField] = + EachNode[F] | AnyNode[F] | ConstNode[F] | BetweenNode[F] | SeveralNode[F] | EveryNode[ + F + ] | FieldNode[F] | FieldNodeWithAny[F] | EnumerableNode[F] | DivisibleNode[F] + import cron4s.syntax.all.toExprOps + def matches[F <: CronField](field: PolyField[F]): Predicate[Int] = field match { + case node: EachNode[F] => node.matches + case node: AnyNode[F] => node.matches + case node: ConstNode[F] => node.matches + case node: BetweenNode[F] => node.matches + case node: SeveralNode[F] => node.matches + case node: EveryNode[F] => node.matches + case node: FieldNode[F] => matches(node.raw) + case node: FieldNodeWithAny[F] => matches(node.raw) + case node: EnumerableNode[F] => matches(node.raw) + case node: DivisibleNode[F] => matches(node.raw) + } + def range[F <: CronField](field: PolyField[F]): IndexedSeq[Int] = field match { + case node: EachNode[F] => node.range + case node: AnyNode[F] => node.range + case node: ConstNode[F] => node.range + case node: BetweenNode[F] => node.range + case node: SeveralNode[F] => node.range + case node: EveryNode[F] => node.range + case node: FieldNode[F] => range(node.raw) + case node: FieldNodeWithAny[F] => range(node.raw) + case node: EnumerableNode[F] => range(node.raw) + case node: DivisibleNode[F] => range(node.raw) + } + def show[F <: CronField](field: PolyField[F]): String = field match { + case n: EachNode[F] => n.show + case n: AnyNode[F] => n.show + case n: ConstNode[F] => n.show + case n: BetweenNode[F] => n.show + case n: SeveralNode[F] => n.show + case n: EveryNode[F] => n.show + case n: FieldNode[F] => n.show + case n: FieldNodeWithAny[F] => n.show + case n: EnumerableNode[F] => n.show + case n: DivisibleNode[F] => n.show + } + def unit[F <: CronField](field: PolyField[F]): CronUnit[F] = field match { + case n: EachNode[F] => n.unit + case n: AnyNode[F] => n.unit + case n: ConstNode[F] => n.unit + case n: BetweenNode[F] => n.unit + case n: SeveralNode[F] => n.unit + case n: EveryNode[F] => n.unit + case n: FieldNode[F] => unit(n.raw) + case n: FieldNodeWithAny[F] => unit(n.raw) + case n: EnumerableNode[F] => unit(n.raw) + case n: DivisibleNode[F] => unit(n.raw) + } +} diff --git a/modules/core/shared/src/main/scala-3/cron4s/expr/package.scala b/modules/core/shared/src/main/scala-3/cron4s/expr/package.scala new file mode 100644 index 00000000..5b09d26f --- /dev/null +++ b/modules/core/shared/src/main/scala-3/cron4s/expr/package.scala @@ -0,0 +1,56 @@ +/* + * Copyright 2017 Antonio Alonso Dominguez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cron4s + + +/** + * Created by alonsodomin on 04/01/2016. + */ +package object expr { + private[expr] type RawFieldNode[F <: CronField] = + EachNode[F] | ConstNode[F] | BetweenNode[F] | SeveralNode[F] | + EveryNode[F] + + private[expr] type RawFieldNodeWithAny[F <: CronField] = + AnyNode[F] | RawFieldNode[F] + + private[expr] type RawEnumerableNode[F <: CronField] = + ConstNode[F] | BetweenNode[F] + extension [F<:CronField](t: RawEnumerableNode[F]) { + private[cron4s] inline def select[T]: Option[T] = t match + case t: T => Some(t) + case _ => None + } + + private[expr] type RawDivisibleNode[F <: CronField] = + EachNode[F] | BetweenNode[F] | SeveralNode[F] + + type SecondsNode = FieldNode[CronField.Second] + type MinutesNode = FieldNode[CronField.Minute] + type HoursNode = FieldNode[CronField.Hour] + type DaysOfMonthNode = FieldNodeWithAny[CronField.DayOfMonth] + type MonthsNode = FieldNode[CronField.Month] + type DaysOfWeekNode = FieldNodeWithAny[CronField.DayOfWeek] + + private[cron4s] type RawTimeCronExpr = + SecondsNode *: MinutesNode *: HoursNode *: EmptyTuple + private[cron4s] type RawDateCronExpr = + DaysOfMonthNode *: MonthsNode *: DaysOfWeekNode *: EmptyTuple + + private[cron4s] type RawCronExpr = + SecondsNode *: MinutesNode *: HoursNode *: DaysOfMonthNode *: MonthsNode *: DaysOfWeekNode *: EmptyTuple +} diff --git a/modules/core/shared/src/main/scala-3/cron4s/expr/parts.scala b/modules/core/shared/src/main/scala-3/cron4s/expr/parts.scala new file mode 100644 index 00000000..7c8e7bda --- /dev/null +++ b/modules/core/shared/src/main/scala-3/cron4s/expr/parts.scala @@ -0,0 +1,50 @@ +/* + * Copyright 2017 Antonio Alonso Dominguez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cron4s.expr + +import cats._ +import cats.implicits._ + +final case class DateCronExpr( + daysOfMonth: DaysOfMonthNode, + months: MonthsNode, + daysOfWeek: DaysOfWeekNode +) { + private[cron4s] lazy val raw: RawDateCronExpr = (daysOfMonth, months, daysOfWeek) + + override lazy val toString: String = + List( + _root_.cron4s.expr.ops.show(daysOfMonth), + _root_.cron4s.expr.ops.show(months), + _root_.cron4s.expr.ops.show(daysOfWeek) + ).mkString(" ") +} + +object DateCronExpr extends DateCronExprInstances + +final case class TimeCronExpr( + seconds: SecondsNode, + minutes: MinutesNode, + hours: HoursNode +) { + private[cron4s] lazy val raw: RawTimeCronExpr = (seconds, minutes, hours) + + override lazy val toString: String = + List(seconds, minutes, hours).map(_root_.cron4s.expr.ops.show).mkString(" ") +} + +object TimeCronExpr extends TimeCronExprInstances diff --git a/modules/core/shared/src/main/scala-3/cron4s/expr/wrappers.scala b/modules/core/shared/src/main/scala-3/cron4s/expr/wrappers.scala new file mode 100644 index 00000000..62c19e33 --- /dev/null +++ b/modules/core/shared/src/main/scala-3/cron4s/expr/wrappers.scala @@ -0,0 +1,49 @@ +/* + * Copyright 2017 Antonio Alonso Dominguez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cron4s.expr + +import cats.syntax.all._ +import cron4s.CronField + +/** + * Created by alonsodomin on 23/01/2017. + */ +final class FieldNode[F <: CronField](private[cron4s] val raw: RawFieldNode[F]) extends AnyVal { + override def toString: String = _root_.cron4s.expr.ops.show(raw) +} + +object FieldNode extends FieldNodeInstances + +final class FieldNodeWithAny[F <: CronField](private[cron4s] val raw: RawFieldNodeWithAny[F]) + extends AnyVal { + override def toString: String = _root_.cron4s.expr.ops.show(raw) +} + +object FieldNodeWithAny extends FieldNodeWithAnyInstances + +final class EnumerableNode[F <: CronField](private[cron4s] val raw: RawEnumerableNode[F]) + extends AnyVal { + override def toString: String = _root_.cron4s.expr.ops.show(raw) +} + +object EnumerableNode extends EnumerableNodeInstances + +final class DivisibleNode[F <: CronField](private[cron4s] val raw: RawDivisibleNode[F]) + extends AnyVal { + override def toString: String = _root_.cron4s.expr.ops.show(raw) +} +object DivisibleNode extends DivisibleNodeInstances diff --git a/modules/core/shared/src/main/scala-3/cron4s/validation/NodeValidators.scala b/modules/core/shared/src/main/scala-3/cron4s/validation/NodeValidators.scala new file mode 100644 index 00000000..ad8c6fa0 --- /dev/null +++ b/modules/core/shared/src/main/scala-3/cron4s/validation/NodeValidators.scala @@ -0,0 +1,187 @@ +/* + * Copyright 2017 Antonio Alonso Dominguez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cron4s.validation + + +import cats.data._ +import cats.implicits._ + +import cron4s.{CronField, CronUnit, InvalidField} +import cron4s.expr._ +import cron4s.base.Enumerated + +/** + * Created by alonsodomin on 18/12/2016. + */ +sealed trait NodeValidator[A] { + def validate(node: A): List[InvalidField] +} + +object NodeValidator extends NodeValidatorInstances { + @inline def apply[A](implicit validator: NodeValidator[A]): NodeValidator[A] = + validator + + def alwaysValid[A]: NodeValidator[A] = + new NodeValidator[A] { + def validate(node: A): List[InvalidField] = List.empty + } +} + +private[validation] trait NodeValidatorInstances extends LowPriorityNodeValidatorInstances { + implicit def eachValidator[F <: CronField]: NodeValidator[EachNode[F]] = + NodeValidator.alwaysValid[EachNode[F]] + + implicit def anyValidator[F <: CronField]: NodeValidator[AnyNode[F]] = + NodeValidator.alwaysValid[AnyNode[F]] + + implicit def constValidator[F <: CronField](implicit + ev: Enumerated[CronUnit[F]] + ): NodeValidator[ConstNode[F]] = + new NodeValidator[ConstNode[F]] { + import scala.language.implicitConversions + import cron4s.syntax.all.toEnumeratedOps + def validate(node: ConstNode[F]): List[InvalidField] = + if (node.value < node.unit.min || node.value > node.unit.max) + List( + InvalidField( + node.unit.field, + s"Value ${node.value} is out of bounds for field: ${node.unit.field}" + ) + ) + else List.empty + } + + implicit def betweenValidator[F <: CronField](implicit + ev: Enumerated[CronUnit[F]] + ): NodeValidator[BetweenNode[F]] = + new NodeValidator[BetweenNode[F]] { + val subValidator = NodeValidator[ConstNode[F]] + + def validate(node: BetweenNode[F]): List[InvalidField] = { + val baseErrors = List( + subValidator.validate(node.begin), + subValidator.validate(node.end) + ).flatten + + if (node.begin.value >= node.end.value) { + val error = InvalidField( + node.unit.field, + s"${node.begin.value} should be less than ${node.end.value}" + ) + error :: baseErrors + } else + baseErrors + } + } + + implicit def severalValidator[F <: CronField](implicit + ev: Enumerated[CronUnit[F]] + ): NodeValidator[SeveralNode[F]] = + new NodeValidator[SeveralNode[F]] { + val elemValidator = NodeValidator[EnumerableNode[F]] + + def implicationErrorMsg(that: EnumerableNode[F], impliedBy: EnumerableNode[F]): String = + s"Value '${that.show}' is implied by '${impliedBy.show}'" + + def checkImplication( + curr: EnumerableNode[F] + ): State[List[EnumerableNode[F]], List[List[InvalidField]]] = { + import scala.language.implicitConversions + import cron4s.syntax.all.toExprOps + lazy val currField = curr.unit.field + + def impliedByError(elem: EnumerableNode[F]): List[InvalidField] = + if (curr.impliedBy(elem)) + List(InvalidField(currField, implicationErrorMsg(curr, elem))) + else Nil + + def impliesError(elem: EnumerableNode[F]): List[InvalidField] = + if (curr.implies(elem)) + List(InvalidField(currField, implicationErrorMsg(elem, curr))) + else Nil + + State { seen => + val errors = seen.foldMap(elem => impliesError(elem) ++ impliedByError(elem)) + (curr :: seen) -> List(errors) + } + } + + def validate(node: SeveralNode[F]): List[InvalidField] = { + val validation = node.values.foldMapM { elem => + val elemErrors = elemValidator.validate(elem) + // If subexpressions in the elements are not valid, then + // do not check for element implication + if (elemErrors.isEmpty) checkImplication(elem) + else + State.pure[List[EnumerableNode[F]], List[List[InvalidField]]](List(elemErrors)) + } + validation.map(_.flatten).runEmptyA.value + } + } + + implicit def everyValidator[F <: CronField](implicit + ev: Enumerated[CronUnit[F]] + ): NodeValidator[EveryNode[F]] = + import cron4s.syntax.enumerated.toEnumeratedOps + new NodeValidator[EveryNode[F]] { + def validate(node: EveryNode[F]): List[InvalidField] = { + lazy val baseErrors = NodeValidator[DivisibleNode[F]].validate(node.base) + val evenlyDivided = (node.base.range.size % node.freq) == 0 + if (!evenlyDivided) + InvalidField( + node.unit.field, + s"Step '${node.freq}' does not evenly divide the value '${node.base.show}'" + ) :: baseErrors + else baseErrors + } + } +} + + +private[validation] trait LowPriorityNodeValidatorInstances { + implicit def enumerableNodeValidator[F <: CronField](implicit + ev: Enumerated[CronUnit[F]] + ): NodeValidator[EnumerableNode[F]] = + new NodeValidator[EnumerableNode[F]] { + def validate(node: EnumerableNode[F]): List[InvalidField] = + _root_.cron4s.validation.ops.validate(node.raw) + } + + implicit def divisibleNodeValidator[F <: CronField](implicit + ev: Enumerated[CronUnit[F]] + ): NodeValidator[DivisibleNode[F]] = + new NodeValidator[DivisibleNode[F]] { + def validate(node: DivisibleNode[F]): List[InvalidField] = + _root_.cron4s.validation.ops.validate(node.raw) + } + + implicit def fieldNodeValidator[F <: CronField](implicit + ev: Enumerated[CronUnit[F]] + ): NodeValidator[FieldNode[F]] = + new NodeValidator[FieldNode[F]] { + def validate(node: FieldNode[F]): List[InvalidField] = + _root_.cron4s.validation.ops.validate(node.raw) + } + + implicit def fieldNodeWithAnyValidator[F <: CronField](implicit + ev: Enumerated[CronUnit[F]] + ): NodeValidator[FieldNodeWithAny[F]] = + new NodeValidator[FieldNodeWithAny[F]] { + def validate(node: FieldNodeWithAny[F]): List[InvalidField] = + _root_.cron4s.validation.ops.validate(node.raw) + } +} diff --git a/modules/core/shared/src/main/scala-3/cron4s/validation/ops.scala b/modules/core/shared/src/main/scala-3/cron4s/validation/ops.scala new file mode 100644 index 00000000..6d0af7bb --- /dev/null +++ b/modules/core/shared/src/main/scala-3/cron4s/validation/ops.scala @@ -0,0 +1,43 @@ +/* + * Copyright 2017 Antonio Alonso Dominguez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cron4s.validation + +import cron4s.CronField +import cron4s.CronUnit +import cron4s.expr._ +import cron4s.base.Enumerated +import cron4s.InvalidField + +/** + * Created by alonsodomin on 03/02/2017. + */ +private[validation] object ops extends NodeValidatorInstances { + type Validatable[F<: CronField] = EachNode[F] | AnyNode[F] | ConstNode[F] | BetweenNode[F] | SeveralNode[ + F + ] | EveryNode[F] | FieldNode[F] | FieldNodeWithAny[F] | Nothing + def validate[F <: CronField](node: Validatable[F])(using Enumerated[CronUnit[F]]): List[InvalidField] = node match { + case field: EachNode[F] => summon[NodeValidator[EachNode[F]]].validate(field) + case field: AnyNode[F] => summon[NodeValidator[AnyNode[F]]].validate(field) + case field: ConstNode[F] => summon[NodeValidator[ConstNode[F]]].validate(field) + case field: BetweenNode[F] => summon[NodeValidator[BetweenNode[F]]].validate(field) + case field: SeveralNode[F] => implicitly[NodeValidator[SeveralNode[F]]].validate(field) + case field: EveryNode[F] => implicitly[NodeValidator[EveryNode[F]]].validate(field) + case field: FieldNode[F] => implicitly[NodeValidator[FieldNode[F]]].validate(field) + case field: FieldNodeWithAny[F] => + implicitly[NodeValidator[FieldNodeWithAny[F]]].validate(field) + } +} diff --git a/modules/core/shared/src/main/scala-3/cron4s/validation/package.scala b/modules/core/shared/src/main/scala-3/cron4s/validation/package.scala new file mode 100644 index 00000000..c43f9156 --- /dev/null +++ b/modules/core/shared/src/main/scala-3/cron4s/validation/package.scala @@ -0,0 +1,67 @@ +/* + * Copyright 2017 Antonio Alonso Dominguez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cron4s + +import cats.data.NonEmptyList + +/** + * Created by alonsodomin on 30/08/2016. + */ +package object validation { + def validateCron(expr: CronExpr): Either[InvalidCron, CronExpr] = { + val dayFieldError = validateDayFields(expr) + val fieldErrors = expr.raw match { + case (seconds, minutes, hours, daysOfMonth, months, daysOfWeek) => + List( + ops.validate(seconds), + ops.validate(minutes), + ops.validate(hours), + ops.validate(daysOfMonth), + ops.validate(months), + ops.validate(daysOfWeek), + ).flatten + } + + val allErrors = + dayFieldError.fold[List[ValidationError]](fieldErrors)(_ :: fieldErrors) + + NonEmptyList + .fromList(allErrors) + .map(errs => Left(InvalidCron(errs))) + .getOrElse(Right(expr)) + } + + private def validateDayFields(expr: CronExpr) = { + val dayOfMonth = expr.field[CronField.DayOfMonth].toString + val dayOfWeek = expr.field[CronField.DayOfWeek].toString + + if (dayOfMonth == dayOfWeek) + Some( + InvalidFieldCombination( + s"Fields ${CronField.DayOfMonth} and ${CronField.DayOfWeek} can't both have the expression: $dayOfMonth" + ) + ) + else if ((dayOfMonth != "?" && dayOfWeek == "?") || (dayOfMonth == "?" && dayOfWeek != "?")) + None + else + Some( + InvalidFieldCombination( + s"Either ${CronField.DayOfMonth} and ${CronField.DayOfWeek} must have a ? expression" + ) + ) + } +} diff --git a/modules/core/shared/src/main/scala/cron4s/Cron.scala b/modules/core/shared/src/main/scala/cron4s/Cron.scala index 9035829c..b1d70e5a 100644 --- a/modules/core/shared/src/main/scala/cron4s/Cron.scala +++ b/modules/core/shared/src/main/scala/cron4s/Cron.scala @@ -17,7 +17,7 @@ package cron4s import scala.scalajs.js.annotation.JSExportTopLevel -import scala.util.{Failure, Success, Try} +import scala.util.Try /** * The entry point for parsing cron expressions diff --git a/modules/core/shared/src/main/scala/cron4s/datetime/DateTimeCron.scala b/modules/core/shared/src/main/scala/cron4s/datetime/DateTimeCron.scala index 6d03c2c2..88e114aa 100644 --- a/modules/core/shared/src/main/scala/cron4s/datetime/DateTimeCron.scala +++ b/modules/core/shared/src/main/scala/cron4s/datetime/DateTimeCron.scala @@ -82,7 +82,7 @@ private[datetime] final class FullCron extends DateTimeCron[CronExpr] { } def ranges(expr: CronExpr): Map[CronField, IndexedSeq[Int]] = - supportedFields.zip(expr.raw.map(ops.range).toList).toMap + supportedFields.zip(applyRange(expr)).toMap @inline val supportedFields: List[CronField] = CronField.All @@ -105,7 +105,7 @@ private[datetime] final class TimeCron extends DateTimeCron[TimeCronExpr] { } def ranges(expr: TimeCronExpr): Map[CronField, IndexedSeq[Int]] = - supportedFields.zip(expr.raw.map(ops.range).toList).toMap + supportedFields.zip(applyRange(expr)).toMap @inline val supportedFields: List[CronField] = @@ -129,7 +129,7 @@ private[datetime] final class DateCron extends DateTimeCron[DateCronExpr] { } def ranges(expr: DateCronExpr): Map[CronField, IndexedSeq[Int]] = - supportedFields.zip(expr.raw.map(ops.range).toList).toMap + supportedFields.zip(applyRange(expr)).toMap @inline val supportedFields: List[CronField] = diff --git a/modules/core/shared/src/main/scala/cron4s/datetime/DateTimeNode.scala b/modules/core/shared/src/main/scala/cron4s/datetime/DateTimeNode.scala index 82556392..ec3b89e7 100644 --- a/modules/core/shared/src/main/scala/cron4s/datetime/DateTimeNode.scala +++ b/modules/core/shared/src/main/scala/cron4s/datetime/DateTimeNode.scala @@ -31,7 +31,6 @@ trait DateTimeNode[E[_ <: CronField], F <: CronField] { */ def matchesIn[DateTime](expr: E[F], DT: IsDateTime[DateTime]): Predicate[DateTime] = Predicate { dt => - import cats.syntax.either._ val current = DT.get(dt, expr.unit.field) current.map(expr.matches).getOrElse(false) } @@ -71,7 +70,6 @@ trait DateTimeNode[E[_ <: CronField], F <: CronField] { expr: E[F], DT: IsDateTime[DateTime] )(dateTime: DateTime, step: Int): Option[DateTime] = { - import cats.syntax.either._ for { current <- DT.get(dateTime, expr.unit.field).toOption newValue <- expr.step(current, step).map(_._1) diff --git a/modules/core/shared/src/main/scala/cron4s/expr/CronExprInstances.scala b/modules/core/shared/src/main/scala/cron4s/expr/CronExprInstances.scala new file mode 100644 index 00000000..2b29ff93 --- /dev/null +++ b/modules/core/shared/src/main/scala/cron4s/expr/CronExprInstances.scala @@ -0,0 +1,35 @@ +/* + * Copyright 2017 Antonio Alonso Dominguez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cron4s.expr + +import cats.{Eq, Show} +import cats.implicits._ + +private[cron4s] trait Cron4sInstances { + implicit val CronExprEq: Eq[CronExpr] = + Eq.instance { (lhs, rhs) => + lhs.seconds === rhs.seconds && + lhs.minutes === rhs.minutes && + lhs.hours === rhs.hours && + lhs.daysOfMonth === rhs.daysOfMonth && + lhs.months === rhs.months && + lhs.daysOfWeek === rhs.daysOfWeek + } + + implicit val CronExprShow: Show[CronExpr] = + Show.fromToString[CronExpr] +} diff --git a/modules/core/shared/src/main/scala/cron4s/expr/DateCronExprInstances.scala b/modules/core/shared/src/main/scala/cron4s/expr/DateCronExprInstances.scala new file mode 100644 index 00000000..c809757f --- /dev/null +++ b/modules/core/shared/src/main/scala/cron4s/expr/DateCronExprInstances.scala @@ -0,0 +1,30 @@ +/* + * Copyright 2017 Antonio Alonso Dominguez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cron4s.expr + +import cats._ +import cats.implicits._ + +private[cron4s] trait DateCronExprInstances { + implicit val dateCronEq: Eq[DateCronExpr] = Eq.instance { (lhs, rhs) => + lhs.daysOfMonth === rhs.daysOfMonth && + lhs.months === rhs.months && + lhs.daysOfWeek === rhs.daysOfWeek + } + implicit val dateCronShow: Show[DateCronExpr] = + Show.fromToString[DateCronExpr] +} diff --git a/modules/core/shared/src/main/scala/cron4s/expr/TimeCronExprInstances.scala b/modules/core/shared/src/main/scala/cron4s/expr/TimeCronExprInstances.scala new file mode 100644 index 00000000..27c376b5 --- /dev/null +++ b/modules/core/shared/src/main/scala/cron4s/expr/TimeCronExprInstances.scala @@ -0,0 +1,30 @@ +/* + * Copyright 2017 Antonio Alonso Dominguez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cron4s.expr +import cats._ +import cats.implicits._ + +private[cron4s] trait TimeCronExprInstances { + implicit val timeCronEq: Eq[TimeCronExpr] = Eq.instance { (lhs, rhs) => + lhs.seconds === rhs.seconds && + lhs.minutes === rhs.minutes && + lhs.hours === rhs.hours + } + + implicit val timeCronShow: Show[TimeCronExpr] = + Show.fromToString[TimeCronExpr] +} diff --git a/modules/core/shared/src/main/scala/cron4s/parsing/package.scala b/modules/core/shared/src/main/scala/cron4s/parsing/package.scala index 9efc340f..c37fed45 100644 --- a/modules/core/shared/src/main/scala/cron4s/parsing/package.scala +++ b/modules/core/shared/src/main/scala/cron4s/parsing/package.scala @@ -16,8 +16,6 @@ package cron4s -import cats.syntax.either._ - package object parsing { private[cron4s] def parse(e: String): Either[Error, CronExpr] = for { diff --git a/modules/momentjs/src/main/scala/cron4s/lib/momentjs/MomentJSInstance.scala b/modules/momentjs/src/main/scala/cron4s/lib/momentjs/MomentJSInstance.scala index c636c00e..2a4a9425 100644 --- a/modules/momentjs/src/main/scala/cron4s/lib/momentjs/MomentJSInstance.scala +++ b/modules/momentjs/src/main/scala/cron4s/lib/momentjs/MomentJSInstance.scala @@ -16,8 +16,6 @@ package cron4s.lib.momentjs -import cats.syntax.either._ - import cron4s.CronField import cron4s.datetime.{DateTimeError, DateTimeUnit, InvalidFieldValue, IsDateTime} diff --git a/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/CronGenerators.scala b/modules/testkit/shared/src/main/scala-2/cron4s/testkit/gen/CronGenerators.scala similarity index 92% rename from modules/testkit/shared/src/main/scala/cron4s/testkit/gen/CronGenerators.scala rename to modules/testkit/shared/src/main/scala-2/cron4s/testkit/gen/CronGenerators.scala index 76bddf4c..25ea6dba 100644 --- a/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/CronGenerators.scala +++ b/modules/testkit/shared/src/main/scala-2/cron4s/testkit/gen/CronGenerators.scala @@ -35,7 +35,7 @@ trait CronGenerators extends NodeGenerators { case _ => anyGen[CronField.DayOfWeek].map(any2FieldWithAny) } - private[this] val fullCronGen = for { + private[this] val fullCronGen: Gen[CronExpr] = for { seconds <- nodeGen[CronField.Second] minutes <- nodeGen[CronField.Minute] hours <- nodeGen[CronField.Hour] @@ -44,13 +44,13 @@ trait CronGenerators extends NodeGenerators { daysOfWeek <- chooseDaysOfWeek(daysOfMonth) } yield CronExpr(seconds, minutes, hours, daysOfMonth, months, daysOfWeek) - private[this] val timeCronGen = for { + private[this] val timeCronGen: Gen[TimeCronExpr] = for { seconds <- nodeGen[CronField.Second] minutes <- nodeGen[CronField.Minute] hours <- nodeGen[CronField.Hour] } yield TimeCronExpr(seconds, minutes, hours) - private[this] val dateCronGen = for { + private[this] val dateCronGen: Gen[DateCronExpr] = for { daysOfMonth <- nodeWithAnyGen[CronField.DayOfMonth] months <- nodeGen[CronField.Month] daysOfWeek <- chooseDaysOfWeek(daysOfMonth) diff --git a/modules/testkit/shared/src/main/scala-3/cron4s/testkit/gen/CronGenerators.scala b/modules/testkit/shared/src/main/scala-3/cron4s/testkit/gen/CronGenerators.scala new file mode 100644 index 00000000..48076888 --- /dev/null +++ b/modules/testkit/shared/src/main/scala-3/cron4s/testkit/gen/CronGenerators.scala @@ -0,0 +1,63 @@ +/* + * Copyright 2017 Antonio Alonso Dominguez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cron4s.testkit.gen + +import cron4s.CronField +import cron4s.expr._ + +import org.scalacheck._ + +/** + * Created by alonsodomin on 29/01/2017. + */ +trait CronGenerators extends NodeGenerators { + private[this] def chooseDaysOfWeek( + daysOfMonth: DaysOfMonthNode + ): Gen[DaysOfWeekNode] = + daysOfMonth.raw match { + case a: AnyNode[_] => nodeGen[CronField.DayOfWeek].map(field2FieldWithAny) // any + case _ => anyGen[CronField.DayOfWeek].map(any2FieldWithAny) + } + + private[this] val fullCronGen: Gen[CronExpr] = for { + seconds <- nodeGen[CronField.Second] + minutes <- nodeGen[CronField.Minute] + hours <- nodeGen[CronField.Hour] + daysOfMonth <- nodeWithAnyGen[CronField.DayOfMonth] + months <- nodeGen[CronField.Month] + daysOfWeek <- chooseDaysOfWeek(daysOfMonth) + } yield CronExpr(seconds, minutes, hours, daysOfMonth, months, daysOfWeek) + + private[this] val timeCronGen: Gen[TimeCronExpr] = for { + seconds <- nodeGen[CronField.Second] + minutes <- nodeGen[CronField.Minute] + hours <- nodeGen[CronField.Hour] + } yield TimeCronExpr(seconds, minutes, hours) + + private[this] val dateCronGen: Gen[DateCronExpr] = for { + daysOfMonth <- nodeWithAnyGen[CronField.DayOfMonth] + months <- nodeGen[CronField.Month] + daysOfWeek <- chooseDaysOfWeek(daysOfMonth) + } yield DateCronExpr(daysOfMonth, months, daysOfWeek) + + implicit lazy val arbitraryFullCron: Arbitrary[CronExpr] = + Arbitrary(fullCronGen) + implicit lazy val arbitraryTimeCron: Arbitrary[TimeCronExpr] = + Arbitrary(timeCronGen) + implicit lazy val arbitraryDateCron: Arbitrary[DateCronExpr] = + Arbitrary(dateCronGen) +} diff --git a/modules/testkit/shared/src/main/scala/cron4s/testkit/CronDateTimeTestKit.scala b/modules/testkit/shared/src/main/scala/cron4s/testkit/CronDateTimeTestKit.scala index feb781c9..929fc86d 100644 --- a/modules/testkit/shared/src/main/scala/cron4s/testkit/CronDateTimeTestKit.scala +++ b/modules/testkit/shared/src/main/scala/cron4s/testkit/CronDateTimeTestKit.scala @@ -21,7 +21,7 @@ import cats.implicits._ import cron4s.Cron import cron4s.datetime.IsDateTime - +import cron4s.syntax.all.toDateTimeCronOps import org.scalatest.flatspec.AnyFlatSpec /** diff --git a/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitraryBetweenNode.scala b/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitraryBetweenNode.scala index 40497f5e..f52a8dc8 100644 --- a/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitraryBetweenNode.scala +++ b/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitraryBetweenNode.scala @@ -16,6 +16,7 @@ package cron4s.testkit.gen +import cron4s.expr.BetweenNode import cron4s.CronField._ import org.scalacheck.Arbitrary @@ -24,10 +25,20 @@ import org.scalacheck.Arbitrary * Created by alonsodomin on 28/08/2016. */ trait ArbitraryBetweenNode extends NodeGenerators { - implicit lazy val arbitraryBetweenSecond = Arbitrary(betweenGen[Second]) - implicit lazy val arbitraryBetweenMinute = Arbitrary(betweenGen[Minute]) - implicit lazy val arbitraryBetweenHour = Arbitrary(betweenGen[Hour]) - implicit lazy val arbitraryBetweenDayOfMonth = Arbitrary(betweenGen[DayOfMonth]) - implicit lazy val arbitraryBetweenMonth = Arbitrary(betweenGen[Month]) - implicit lazy val arbitraryBetweenDayOfWeek = Arbitrary(betweenGen[DayOfWeek]) + implicit lazy val arbitraryBetweenSecond: Arbitrary[BetweenNode[Second]] = Arbitrary( + betweenGen[Second] + ) + implicit lazy val arbitraryBetweenMinute: Arbitrary[BetweenNode[Minute]] = Arbitrary( + betweenGen[Minute] + ) + implicit lazy val arbitraryBetweenHour: Arbitrary[BetweenNode[Hour]] = Arbitrary(betweenGen[Hour]) + implicit lazy val arbitraryBetweenDayOfMonth: Arbitrary[BetweenNode[DayOfMonth]] = Arbitrary( + betweenGen[DayOfMonth] + ) + implicit lazy val arbitraryBetweenMonth: Arbitrary[BetweenNode[Month]] = Arbitrary( + betweenGen[Month] + ) + implicit lazy val arbitraryBetweenDayOfWeek: Arbitrary[BetweenNode[DayOfWeek]] = Arbitrary( + betweenGen[DayOfWeek] + ) } diff --git a/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitraryConstNode.scala b/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitraryConstNode.scala index ada3640e..93a60702 100644 --- a/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitraryConstNode.scala +++ b/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitraryConstNode.scala @@ -17,6 +17,7 @@ package cron4s.testkit.gen import cron4s.CronField._ +import cron4s.expr.ConstNode import org.scalacheck.Arbitrary @@ -24,10 +25,14 @@ import org.scalacheck.Arbitrary * Created by alonsodomin on 28/08/2016. */ trait ArbitraryConstNode extends NodeGenerators { - implicit lazy val arbitraryConstSecond = Arbitrary(constGen[Second]) - implicit lazy val arbitraryConstMinute = Arbitrary(constGen[Minute]) - implicit lazy val arbitraryConstHour = Arbitrary(constGen[Hour]) - implicit lazy val arbitraryConstDayOfMonth = Arbitrary(constGen[DayOfMonth]) - implicit lazy val arbitraryConstMonth = Arbitrary(constGen[Month]) - implicit lazy val arbitraryConstDayOfWeek = Arbitrary(constGen[DayOfWeek]) + implicit lazy val arbitraryConstSecond: Arbitrary[ConstNode[Second]] = Arbitrary(constGen[Second]) + implicit lazy val arbitraryConstMinute: Arbitrary[ConstNode[Minute]] = Arbitrary(constGen[Minute]) + implicit lazy val arbitraryConstHour: Arbitrary[ConstNode[Hour]] = Arbitrary(constGen[Hour]) + implicit lazy val arbitraryConstDayOfMonth: Arbitrary[ConstNode[DayOfMonth]] = Arbitrary( + constGen[DayOfMonth] + ) + implicit lazy val arbitraryConstMonth: Arbitrary[ConstNode[Month]] = Arbitrary(constGen[Month]) + implicit lazy val arbitraryConstDayOfWeek: Arbitrary[ConstNode[DayOfWeek]] = Arbitrary( + constGen[DayOfWeek] + ) } diff --git a/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitraryCronUnits.scala b/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitraryCronUnits.scala index f2f2187d..af2e6e5c 100644 --- a/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitraryCronUnits.scala +++ b/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitraryCronUnits.scala @@ -25,10 +25,22 @@ import org.scalacheck._ * Created by alonsodomin on 28/08/2016. */ trait ArbitraryCronUnits { - implicit lazy val arbitrarySecondsUnit = Arbitrary(Gen.const(CronUnit[Second])) - implicit lazy val arbitraryMinutesUnit = Arbitrary(Gen.const(CronUnit[Minute])) - implicit lazy val arbitraryHoursUnit = Arbitrary(Gen.const(CronUnit[Hour])) - implicit lazy val arbitraryDaysOfMonthUnit = Arbitrary(Gen.const(CronUnit[DayOfMonth])) - implicit lazy val arbitraryMonthsUnit = Arbitrary(Gen.const(CronUnit[Month])) - implicit lazy val arbitraryDaysOfWeekUnit = Arbitrary(Gen.const(CronUnit[DayOfWeek])) + implicit lazy val arbitrarySecondsUnit: Arbitrary[CronUnit[Second]] = Arbitrary( + Gen.const(CronUnit[Second]) + ) + implicit lazy val arbitraryMinutesUnit: Arbitrary[CronUnit[Minute]] = Arbitrary( + Gen.const(CronUnit[Minute]) + ) + implicit lazy val arbitraryHoursUnit: Arbitrary[CronUnit[Hour]] = Arbitrary( + Gen.const(CronUnit[Hour]) + ) + implicit lazy val arbitraryDaysOfMonthUnit: Arbitrary[CronUnit[DayOfMonth]] = Arbitrary( + Gen.const(CronUnit[DayOfMonth]) + ) + implicit lazy val arbitraryMonthsUnit: Arbitrary[CronUnit[Month]] = Arbitrary( + Gen.const(CronUnit[Month]) + ) + implicit lazy val arbitraryDaysOfWeekUnit: Arbitrary[CronUnit[DayOfWeek]] = Arbitrary( + Gen.const(CronUnit[DayOfWeek]) + ) } diff --git a/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitraryEachNode.scala b/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitraryEachNode.scala index dc10f5e8..2cd22cfa 100644 --- a/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitraryEachNode.scala +++ b/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitraryEachNode.scala @@ -17,6 +17,7 @@ package cron4s.testkit.gen import cron4s.CronField._ +import cron4s.expr.EachNode import org.scalacheck.Arbitrary @@ -24,10 +25,14 @@ import org.scalacheck.Arbitrary * Created by alonsodomin on 28/08/2016. */ trait ArbitraryEachNode extends NodeGenerators { - implicit lazy val arbitraryEachSecond = Arbitrary(eachGen[Second]) - implicit lazy val arbitraryEachMinute = Arbitrary(eachGen[Minute]) - implicit lazy val arbitraryEachHour = Arbitrary(eachGen[Hour]) - implicit lazy val arbitraryEachDayOfMonth = Arbitrary(eachGen[DayOfMonth]) - implicit lazy val arbitraryEachMonth = Arbitrary(eachGen[Month]) - implicit lazy val arbitraryEachDayOfWeek = Arbitrary(eachGen[DayOfWeek]) + implicit lazy val arbitraryEachSecond: Arbitrary[EachNode[Second]] = Arbitrary(eachGen[Second]) + implicit lazy val arbitraryEachMinute: Arbitrary[EachNode[Minute]] = Arbitrary(eachGen[Minute]) + implicit lazy val arbitraryEachHour: Arbitrary[EachNode[Hour]] = Arbitrary(eachGen[Hour]) + implicit lazy val arbitraryEachDayOfMonth: Arbitrary[EachNode[DayOfMonth]] = Arbitrary( + eachGen[DayOfMonth] + ) + implicit lazy val arbitraryEachMonth: Arbitrary[EachNode[Month]] = Arbitrary(eachGen[Month]) + implicit lazy val arbitraryEachDayOfWeek: Arbitrary[EachNode[DayOfWeek]] = Arbitrary( + eachGen[DayOfWeek] + ) } diff --git a/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitraryEveryNode.scala b/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitraryEveryNode.scala index 099f0c41..64cfe8e6 100644 --- a/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitraryEveryNode.scala +++ b/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitraryEveryNode.scala @@ -17,17 +17,21 @@ package cron4s.testkit.gen import cron4s.CronField._ - +import cron4s.expr.EveryNode import org.scalacheck.Arbitrary /** * Created by alonsodomin on 28/08/2016. */ trait ArbitraryEveryNode extends NodeGenerators { - implicit lazy val arbitraryEverySecond = Arbitrary(everyGen[Second]) - implicit lazy val arbitraryEveryMinute = Arbitrary(everyGen[Minute]) - implicit lazy val arbitraryEveryHour = Arbitrary(everyGen[Hour]) - implicit lazy val arbitraryEveryDayOfMonth = Arbitrary(everyGen[DayOfMonth]) - implicit lazy val arbitraryEveryMonth = Arbitrary(everyGen[Month]) - implicit lazy val arbitraryEveryDayOfWeek = Arbitrary(everyGen[DayOfWeek]) + implicit lazy val arbitraryEverySecond: Arbitrary[EveryNode[Second]] = Arbitrary(everyGen[Second]) + implicit lazy val arbitraryEveryMinute: Arbitrary[EveryNode[Minute]] = Arbitrary(everyGen[Minute]) + implicit lazy val arbitraryEveryHour: Arbitrary[EveryNode[Hour]] = Arbitrary(everyGen[Hour]) + implicit lazy val arbitraryEveryDayOfMonth: Arbitrary[EveryNode[DayOfMonth]] = Arbitrary( + everyGen[DayOfMonth] + ) + implicit lazy val arbitraryEveryMonth: Arbitrary[EveryNode[Month]] = Arbitrary(everyGen[Month]) + implicit lazy val arbitraryEveryDayOfWeek: Arbitrary[EveryNode[DayOfWeek]] = Arbitrary( + everyGen[DayOfWeek] + ) } diff --git a/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitrarySeveralNode.scala b/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitrarySeveralNode.scala index c9c68857..e58a5727 100644 --- a/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitrarySeveralNode.scala +++ b/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitrarySeveralNode.scala @@ -17,6 +17,7 @@ package cron4s.testkit.gen import cron4s.CronField._ +import cron4s.expr.SeveralNode import org.scalacheck.Arbitrary @@ -24,10 +25,20 @@ import org.scalacheck.Arbitrary * Created by alonsodomin on 28/08/2016. */ trait ArbitrarySeveralNode extends NodeGenerators { - implicit lazy val arbitrarySeveralSecond = Arbitrary(severalGen[Second]) - implicit lazy val arbitrarySeveralMinute = Arbitrary(severalGen[Minute]) - implicit lazy val arbitrarySeveralHour = Arbitrary(severalGen[Hour]) - implicit lazy val arbitrarySeveralDayOfMonth = Arbitrary(severalGen[DayOfMonth]) - implicit lazy val arbitrarySeveralMonth = Arbitrary(severalGen[Month]) - implicit lazy val arbitrarySeveralDayOfWeek = Arbitrary(severalGen[DayOfWeek]) + implicit lazy val arbitrarySeveralSecond: Arbitrary[SeveralNode[Second]] = Arbitrary( + severalGen[Second] + ) + implicit lazy val arbitrarySeveralMinute: Arbitrary[SeveralNode[Minute]] = Arbitrary( + severalGen[Minute] + ) + implicit lazy val arbitrarySeveralHour: Arbitrary[SeveralNode[Hour]] = Arbitrary(severalGen[Hour]) + implicit lazy val arbitrarySeveralDayOfMonth: Arbitrary[SeveralNode[DayOfMonth]] = Arbitrary( + severalGen[DayOfMonth] + ) + implicit lazy val arbitrarySeveralMonth: Arbitrary[SeveralNode[Month]] = Arbitrary( + severalGen[Month] + ) + implicit lazy val arbitrarySeveralDayOfWeek: Arbitrary[SeveralNode[DayOfWeek]] = Arbitrary( + severalGen[DayOfWeek] + ) } diff --git a/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitratyAnyNode.scala b/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitratyAnyNode.scala index f3e086f7..43198032 100644 --- a/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitratyAnyNode.scala +++ b/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/ArbitratyAnyNode.scala @@ -15,6 +15,7 @@ */ package cron4s.testkit.gen +import cron4s.expr.AnyNode import cron4s.CronField._ @@ -24,10 +25,14 @@ import org.scalacheck.Arbitrary * Created by alonsodomin on 10/02/2017. */ trait ArbitratyAnyNode extends NodeGenerators { - implicit lazy val arbitraryAnySecond = Arbitrary(anyGen[Second]) - implicit lazy val arbitraryAnyMinute = Arbitrary(anyGen[Minute]) - implicit lazy val arbitraryAnyHour = Arbitrary(anyGen[Hour]) - implicit lazy val arbitraryAnyDayOfMonth = Arbitrary(anyGen[DayOfMonth]) - implicit lazy val arbitraryAnyMonth = Arbitrary(anyGen[Month]) - implicit lazy val arbitraryAnyDayOfWeek = Arbitrary(anyGen[DayOfWeek]) + implicit lazy val arbitraryAnySecond: Arbitrary[AnyNode[Second]] = Arbitrary(anyGen[Second]) + implicit lazy val arbitraryAnyMinute: Arbitrary[AnyNode[Minute]] = Arbitrary(anyGen[Minute]) + implicit lazy val arbitraryAnyHour: Arbitrary[AnyNode[Hour]] = Arbitrary(anyGen[Hour]) + implicit lazy val arbitraryAnyDayOfMonth: Arbitrary[AnyNode[DayOfMonth]] = Arbitrary( + anyGen[DayOfMonth] + ) + implicit lazy val arbitraryAnyMonth: Arbitrary[AnyNode[Month]] = Arbitrary(anyGen[Month]) + implicit lazy val arbitraryAnyDayOfWeek: Arbitrary[AnyNode[DayOfWeek]] = Arbitrary( + anyGen[DayOfWeek] + ) } diff --git a/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/NodeGenerators.scala b/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/NodeGenerators.scala index 7a05718b..5c7aadc6 100644 --- a/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/NodeGenerators.scala +++ b/modules/testkit/shared/src/main/scala/cron4s/testkit/gen/NodeGenerators.scala @@ -19,8 +19,9 @@ package cron4s.testkit.gen import cron4s.{CronField, CronUnit} import cron4s.expr._ import cron4s.base._ - import org.scalacheck._ +import cron4s.syntax.all.toExprOps +import cron4s.syntax.all.toEnumeratedOps /** * Created by alonsodomin on 28/08/2016. diff --git a/modules/testkit/shared/src/main/scala/cron4s/testkit/laws/DateTimeCronLaws.scala b/modules/testkit/shared/src/main/scala/cron4s/testkit/laws/DateTimeCronLaws.scala index ad075d7d..aca61071 100644 --- a/modules/testkit/shared/src/main/scala/cron4s/testkit/laws/DateTimeCronLaws.scala +++ b/modules/testkit/shared/src/main/scala/cron4s/testkit/laws/DateTimeCronLaws.scala @@ -17,7 +17,6 @@ package cron4s.testkit.laws import cats.laws._ -import cats.syntax.either._ import cron4s.CronField import cron4s.datetime.{DateTimeCron, IsDateTime} @@ -76,7 +75,7 @@ object DateTimeCronLaws { TC0: DateTimeCron[E] ): DateTimeCronLaws[E, DateTime] = new DateTimeCronLaws[E, DateTime] { - implicit val DT = dt0 - implicit val TC = TC0 + implicit def DT: IsDateTime[DateTime] = dt0 + implicit def TC: DateTimeCron[E] = TC0 } } diff --git a/modules/testkit/shared/src/main/scala/cron4s/testkit/laws/DateTimeNodeLaws.scala b/modules/testkit/shared/src/main/scala/cron4s/testkit/laws/DateTimeNodeLaws.scala index f778683a..1f7ae8c2 100644 --- a/modules/testkit/shared/src/main/scala/cron4s/testkit/laws/DateTimeNodeLaws.scala +++ b/modules/testkit/shared/src/main/scala/cron4s/testkit/laws/DateTimeNodeLaws.scala @@ -17,7 +17,6 @@ package cron4s.testkit.laws import cats.laws._ -import cats.syntax.either._ import cron4s.CronField import cron4s.datetime.{IsDateTime, DateTimeNode} @@ -51,8 +50,8 @@ object DateTimeNodeLaws { TC0: DateTimeNode[E, F] ): DateTimeNodeLaws[E, F, DateTime] = new DateTimeNodeLaws[E, F, DateTime] { - implicit val DT = dt0 - implicit val expr = expr0 - implicit val TC = TC0 + implicit def DT: IsDateTime[DateTime] = dt0 + implicit def expr: FieldExpr[E, F] = expr0 + implicit def TC: DateTimeNode[E, F] = TC0 } } diff --git a/project/Dependencies.scala b/project/Dependencies.scala index a2b022b3..2f57b58d 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -33,10 +33,12 @@ object Dependencies { lazy val core = Def.settings( libraryDependencies ++= Seq( - "com.chuusai" %%% "shapeless" % version.shapeless, "org.typelevel" %%% "cats-core" % version.cats.main, "org.scala-lang.modules" %%% "scala-parser-combinators" % version.parserc - ) + ), + libraryDependencies ++= (if (scalaVersion.value.startsWith("2.")) + Seq("com.chuusai" %%% "shapeless" % version.shapeless) + else Seq.empty) ) lazy val coreJS = Def.settings { @@ -56,10 +58,6 @@ object Dependencies { ) } - lazy val tests = Def.settings { - libraryDependencies ++= Seq() - } - lazy val testsJS = Def.settings { libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % version.scalaJavaTime } @@ -90,7 +88,8 @@ object Dependencies { "moment-timezone" -> version.momenttz ) lazy val momentjs = Def.settings( - libraryDependencies += "ru.pavkin" %%% "scala-js-momentjs" % version.momentjs, + libraryDependencies += ("ru.pavkin" %%% "scala-js-momentjs" % version.momentjs) + .cross(CrossVersion.for3Use2_13), Compile / npmDependencies ++= momentjsNpmDeps, Test / npmDependencies ++= momentjsNpmDeps ) diff --git a/project/GithubWorkflow.scala b/project/GithubWorkflow.scala index 248a52c4..221f684d 100644 --- a/project/GithubWorkflow.scala +++ b/project/GithubWorkflow.scala @@ -1,14 +1,18 @@ import sbtghactions.GenerativePlugin.autoImport._ object GithubWorkflow { - val DefaultJVM = JavaSpec(JavaSpec.Distribution.Adopt,"8") + val DefaultJVM = JavaSpec(JavaSpec.Distribution.Adopt, "8") val JvmCond = s"matrix.platform == 'jvm'" val JsCond = s"matrix.platform == 'js'" def settings = Seq( - githubWorkflowJavaVersions := Seq(DefaultJVM, JavaSpec(JavaSpec.Distribution.Adopt,"11"),JavaSpec(JavaSpec.Distribution.Temurin,"17")), + githubWorkflowJavaVersions := Seq( + DefaultJVM, + JavaSpec(JavaSpec.Distribution.Adopt, "11"), + JavaSpec(JavaSpec.Distribution.Temurin, "17") + ), githubWorkflowTargetBranches := Seq("master"), githubWorkflowTargetTags ++= Seq("v*"), githubWorkflowPublishTargetBranches := Seq(RefPredicate.StartsWith(Ref.Tag("v"))), diff --git a/project/build.properties b/project/build.properties index 46e43a97..40b3b8e7 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.8.2 +sbt.version=1.9.0 diff --git a/project/plugins.sbt b/project/plugins.sbt index 03435dfa..d2d1922a 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,15 +1,15 @@ resolvers += "Typesafe Repository" at "https://repo.typesafe.com/typesafe/releases/" -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.1") -addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.2.0") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.7") -addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.4") -addSbtPlugin("com.47deg" % "sbt-microsites" % "1.4.3") -addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.5.0") -addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.2") -addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.9.0") -addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") -addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12") -addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.21.1") -addSbtPlugin("com.codecommit" % "sbt-github-actions" % "0.14.2") -libraryDependencySchemes += "org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.1") +addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.2.0") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.7") +addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.4") +addSbtPlugin("com.47deg" % "sbt-microsites" % "1.4.3") +addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.5.0") +addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.2") +addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.9.0") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") +addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12") +addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.21.1") +addSbtPlugin("com.codecommit" % "sbt-github-actions" % "0.14.2") +libraryDependencySchemes += "org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always diff --git a/tests/shared/src/test/scala/cron4s/CronSpec.scala b/tests/shared/src/test/scala/cron4s/CronSpec.scala index 7cfe03d7..659d6c48 100644 --- a/tests/shared/src/test/scala/cron4s/CronSpec.scala +++ b/tests/shared/src/test/scala/cron4s/CronSpec.scala @@ -19,7 +19,6 @@ package cron4s import cats.data.NonEmptyList import cron4s.expr._ -import cron4s.syntax.all._ import org.scalatest.matchers.should.Matchers import org.scalatest.flatspec.AnyFlatSpec @@ -94,7 +93,7 @@ class CronSpec extends AnyFlatSpec with Matchers { import CronSpec._ "Cron" should "not parse an invalid expression" in { - val expectedError = + val _ = InvalidFieldCombination("Fields DayOfMonth and DayOfWeek can't both have the expression: *") forAll(InvalidExprs) { (desc: String, expr: String, err: Error) => diff --git a/tests/shared/src/test/scala/cron4s/base/PredicateSpec.scala b/tests/shared/src/test/scala/cron4s/base/PredicateSpec.scala index 8c79251c..93b16886 100644 --- a/tests/shared/src/test/scala/cron4s/base/PredicateSpec.scala +++ b/tests/shared/src/test/scala/cron4s/base/PredicateSpec.scala @@ -31,11 +31,11 @@ import org.scalacheck._ class PredicateSpec extends Cron4sLawSuite { import Arbitrary._ - implicit lazy val arbitraryPredicate = Arbitrary[Predicate[Int]] { + implicit lazy val arbitraryPredicate: Arbitrary[Predicate[Int]] = Arbitrary[Predicate[Int]] { for { x <- arbitrary[Int] } yield equalTo(x) } - implicit val predicateEq = Eq.by[Predicate[Int], Boolean](_.apply(0)) + implicit val predicateEq: Eq[Predicate[Int]] = Eq.by[Predicate[Int], Boolean](_.apply(0)) checkAll("ContravariantPredicate", ContravariantTests[Predicate].contravariant[Int, Int, Int]) checkAll( diff --git a/tests/shared/src/test/scala/cron4s/parsing/ParserSpec.scala b/tests/shared/src/test/scala/cron4s/parsing/ParserSpec.scala index 51918952..1052e9e4 100644 --- a/tests/shared/src/test/scala/cron4s/parsing/ParserSpec.scala +++ b/tests/shared/src/test/scala/cron4s/parsing/ParserSpec.scala @@ -17,8 +17,6 @@ package cron4s package parsing -import cats.syntax.either._ - import cron4s.expr._ import cron4s.testkit.Cron4sPropSpec import cron4s.testkit.gen.{ArbitraryEachNode, NodeGenerators}