Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fs2-cron-cron-utils module #412

Merged
merged 5 commits into from
Mar 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,11 @@ jobs:

- name: Make target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master')
run: mkdir -p target .js/target modules/cron4s/.jvm/target .jvm/target .native/target modules/readme/target modules/core/.jvm/target modules/calev/.jvm/target project/target
run: mkdir -p target .js/target modules/cron4s/.jvm/target .jvm/target .native/target modules/cron-utils/.jvm/target modules/readme/target modules/core/.jvm/target modules/calev/.jvm/target project/target

- name: Compress target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master')
run: tar cf targets.tar target .js/target modules/cron4s/.jvm/target .jvm/target .native/target modules/readme/target modules/core/.jvm/target modules/calev/.jvm/target project/target
run: tar cf targets.tar target .js/target modules/cron4s/.jvm/target .jvm/target .native/target modules/cron-utils/.jvm/target modules/readme/target modules/core/.jvm/target modules/calev/.jvm/target project/target

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master')
Expand Down
51 changes: 29 additions & 22 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import sbtcrossproject.{CrossProject, CrossType, Platform}
import org.typelevel.sbt.gha.JavaSpec.Distribution.Temurin

/// variables

Expand All @@ -13,9 +12,10 @@ val Scala_2_13 = "2.13.10"
val Scala_3 = "3.2.2"

val moduleCrossPlatformMatrix: Map[String, List[Platform]] = Map(
"calev" -> List(JVMPlatform),
"core" -> List(JVMPlatform),
"cron4s" -> List(JVMPlatform),
"calev" -> List(JVMPlatform)
"cron-utils" -> List(JVMPlatform)
)

/// global settings
Expand Down Expand Up @@ -66,7 +66,7 @@ ThisBuild / mergifyPrRules := {
/// projects

lazy val root = tlCrossRootProject
.aggregate(calev, core, cron4s, readme)
.aggregate(calev, core, cron4s, cronUtils, readme)

lazy val core = myCrossProject("core")
.settings(
Expand All @@ -75,53 +75,60 @@ lazy val core = myCrossProject("core")
)
)

lazy val coreJVM = core.jvm

lazy val cron4s = myCrossProject("cron4s")
lazy val calev = myCrossProject("calev")
.dependsOn(core)
.settings(
crossScalaVersions := List(Scala_2_12, Scala_2_13),
libraryDependencies ++= Seq(
Dependencies.cron4s,
Dependencies.fs2Core,
Dependencies.scalaTest % Test
Dependencies.calevCore,
Dependencies.munitCatsEffect % Test
),
initialCommands := s"""
import $rootPkg._
import cats.effect.unsafe.implicits.global
import $rootPkg.calev._
import cats.effect.IO
import _root_.cron4s.Cron
import cats.effect.unsafe.implicits.global
import com.github.eikek.calev._
import fs2.Stream
import scala.concurrent.ExecutionContext
"""
)

lazy val cron4sJVM = cron4s.jvm

lazy val calev = myCrossProject("calev")
lazy val cron4s = myCrossProject("cron4s")
.dependsOn(core)
.settings(
crossScalaVersions := List(Scala_2_12, Scala_2_13),
libraryDependencies ++= Seq(
Dependencies.calevCore,
Dependencies.scalaTest % Test
Dependencies.cron4s,
Dependencies.munitCatsEffect % Test
),
initialCommands := s"""
import $rootPkg._
import $rootPkg.calev._
import cats.effect.IO
import cats.effect.unsafe.implicits.global
import com.github.eikek.calev._
import cats.effect.IO
import _root_.cron4s.Cron
import fs2.Stream
import scala.concurrent.ExecutionContext
"""
)

lazy val calevJVM = calev.jvm
lazy val cronUtils = myCrossProject("cron-utils")
.dependsOn(core)
.settings(
libraryDependencies ++= Seq(
Dependencies.cronUtils,
Dependencies.munitCatsEffect % Test
),
tlVersionIntroduced := Map(
"2.12" -> "0.8.2",
"2.13" -> "0.8.2",
"3" -> "0.8.2"
)
)

lazy val readme = project
.in(file("modules/readme"))
.enablePlugins(MdocPlugin, NoPublishPlugin)
.dependsOn(calevJVM, cron4sJVM)
.dependsOn(calev.jvm, cron4s.jvm)
.settings(commonSettings)
.settings(
crossScalaVersions := List(Scala_2_12, Scala_2_13),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@
package eu.timepit.fs2cron.calev

import cats.effect.IO
import cats.effect.unsafe.implicits.global
import com.github.eikek.calev.CalEvent
import fs2.Stream
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import munit.CatsEffectSuite

import java.time.{Instant, ZoneId, ZoneOffset}

class CalevSchedulerTest extends AnyFunSuite with Matchers {
class CalevSchedulerTest extends CatsEffectSuite {
private val everySecond: CalEvent = CalEvent.unsafe("*-*-* *:*:*")
private val evenSeconds: CalEvent = CalEvent.unsafe("*-*-* *:*:0/2")

private def isEven(i: Long): Boolean = i % 2 == 0
private def instantSeconds(i: Instant): Long = i.getEpochSecond
private val evalInstantNow: Stream[IO, Instant] = Stream.eval(IO(Instant.now()))
Expand All @@ -37,26 +37,25 @@ class CalevSchedulerTest extends AnyFunSuite with Matchers {
test("awakeEvery") {
val s1 = schedulerSys.awakeEvery(evenSeconds) >> evalInstantNow
val s2 = s1.map(instantSeconds).take(2).forall(isEven)
s2.compile.last.map(_ should be(Option(true))).unsafeRunSync()
assertIO(s2.compile.last, Some(true))
}

test("sleep") {
val s1 = schedulerUtc.sleep(evenSeconds) >> evalInstantNow
val s2 = s1.map(instantSeconds).forall(isEven)
s2.compile.last.map(_ should be(Option(true))).unsafeRunSync()
assertIO(s2.compile.last, Some(true))
}

test("schedule") {
val everySecond: CalEvent = CalEvent.unsafe("*-*-* *:*:*")
val s1 = schedulerSys
.schedule(List(everySecond -> evalInstantNow, evenSeconds -> evalInstantNow))
.map(instantSeconds)

(for {
for {
seconds <- s1.take(3).compile.toList
_ <- IO(seconds.count(isEven) shouldBe 2)
_ <- IO(seconds.count(!isEven(_)) shouldBe 1)
} yield ()).unsafeRunSync()
_ = assertEquals(seconds.count(isEven), 2)
_ = assertEquals(seconds.count(!isEven(_)), 1)
} yield ()
}

test("timezones") {
Expand All @@ -65,6 +64,6 @@ class CalevSchedulerTest extends AnyFunSuite with Matchers {

val s1 = scheduler.awakeEvery(evenSeconds) >> evalInstantNow
val s2 = s1.map(instantSeconds).take(2).forall(!isEven(_))
s2.compile.last.map(_ should be(Option(true))).unsafeRunSync()
assertIO(s2.compile.last, Some(true))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2018-2021 fs2-cron contributors
*
* 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 eu.timepit.fs2cron.cronutils

import cats.effect.{Sync, Temporal}
import com.cronutils.model.time.ExecutionTime
import eu.timepit.fs2cron.{Scheduler, ZonedDateTimeScheduler}

import java.time.{ZoneId, ZoneOffset, ZonedDateTime}

object CronUtilsScheduler {
def systemDefault[F[_]](implicit temporal: Temporal[F], F: Sync[F]): Scheduler[F, ExecutionTime] =
from(F.delay(ZoneId.systemDefault()))

def utc[F[_]](implicit F: Temporal[F]): Scheduler[F, ExecutionTime] =
from(F.pure(ZoneOffset.UTC))

def from[F[_]](zoneId: F[ZoneId])(implicit F: Temporal[F]): Scheduler[F, ExecutionTime] =
new ZonedDateTimeScheduler[F, ExecutionTime](zoneId) {
override def next(from: ZonedDateTime, schedule: ExecutionTime): F[ZonedDateTime] =
schedule.nextExecution(from).map[F[ZonedDateTime]](zdt => F.pure(zdt)).orElse {
val msg = s"Could not calculate the next date-time from $from " +
s"given the cron expression '$schedule'."
F.raiseError(new Throwable(msg))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright 2018-2021 fs2-cron contributors
*
* 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 eu.timepit.fs2cron.cronutils

import cats.effect.IO
import com.cronutils.model.CronType
import com.cronutils.model.definition.CronDefinitionBuilder
import com.cronutils.model.time.ExecutionTime
import com.cronutils.parser.CronParser
import fs2.Stream
import munit.CatsEffectSuite

import java.time.{Instant, ZoneId, ZoneOffset}

class CronUtilsSchedulerTest extends CatsEffectSuite {
private val cronDef = CronDefinitionBuilder.instanceDefinitionFor(CronType.SPRING)
private val parser = new CronParser(cronDef)

private val everySecond = ExecutionTime.forCron(parser.parse("* * * ? * *"))
private val evenSeconds = ExecutionTime.forCron(parser.parse("*/2 * * ? * *"))

private def isEven(i: Long): Boolean = i % 2 == 0
private def instantSeconds(i: Instant): Long = i.getEpochSecond
private val evalInstantNow: Stream[IO, Instant] = Stream.eval(IO(Instant.now()))

private val schedulerSys = CronUtilsScheduler.systemDefault[IO]
private val schedulerUtc = CronUtilsScheduler.utc[IO]

test("awakeEvery") {
val s1 = schedulerSys.awakeEvery(evenSeconds) >> evalInstantNow
val s2 = s1.map(instantSeconds).take(2).forall(isEven)
assertIO(s2.compile.last, Some(true))
}

test("sleep") {
val s1 = schedulerUtc.sleep(evenSeconds) >> evalInstantNow
val s2 = s1.map(instantSeconds).forall(isEven)
assertIO(s2.compile.last, Some(true))
}

test("schedule") {
val s1 = schedulerSys
.schedule(List(everySecond -> evalInstantNow, evenSeconds -> evalInstantNow))
.map(instantSeconds)

for {
seconds <- s1.take(3).compile.toList
_ = assertEquals(seconds.count(isEven), 2)
_ = assertEquals(seconds.count(!isEven(_)), 1)
} yield ()
}

test("timezones") {
val zoneId: ZoneId = ZoneOffset.ofTotalSeconds(1)
val scheduler = CronUtilsScheduler.from(IO.pure(zoneId))

val s1 = scheduler.awakeEvery(evenSeconds) >> evalInstantNow
val s2 = s1.map(instantSeconds).take(2).forall(!isEven(_))
assertIO(s2.compile.last, Some(true))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@
package eu.timepit.fs2cron.cron4s

import cats.effect.IO
import cats.effect.unsafe.implicits.global
import cron4s.Cron
import cron4s.expr.CronExpr
import fs2.Stream
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import munit.CatsEffectSuite

import java.time.{Instant, ZoneId, ZoneOffset}

class Cron4sSchedulerTest extends AnyFunSuite with Matchers {
class Cron4sSchedulerTest extends CatsEffectSuite {
private val everySecond: CronExpr = Cron.unsafeParse("* * * ? * *")
private val evenSeconds: CronExpr = Cron.unsafeParse("*/2 * * ? * *")

private def isEven(i: Long): Boolean = i % 2 == 0
private def instantSeconds(i: Instant): Long = i.getEpochSecond
private val evalInstantNow: Stream[IO, Instant] = Stream.eval(IO(Instant.now()))
Expand All @@ -38,26 +38,25 @@ class Cron4sSchedulerTest extends AnyFunSuite with Matchers {
test("awakeEvery") {
val s1 = schedulerSys.awakeEvery(evenSeconds) >> evalInstantNow
val s2 = s1.map(instantSeconds).take(2).forall(isEven)
s2.compile.last.map(_ should be(Option(true))).unsafeRunSync()
assertIO(s2.compile.last, Some(true))
}

test("sleep") {
val s1 = schedulerUtc.sleep(evenSeconds) >> evalInstantNow
val s2 = s1.map(instantSeconds).forall(isEven)
s2.compile.last.map(_ should be(Option(true))).unsafeRunSync()
assertIO(s2.compile.last, Some(true))
}

test("schedule") {
val everySecond: CronExpr = Cron.unsafeParse("* * * ? * *")
val s1 = schedulerSys
.schedule(List(everySecond -> evalInstantNow, evenSeconds -> evalInstantNow))
.map(instantSeconds)

(for {
for {
seconds <- s1.take(3).compile.toList
_ <- IO(seconds.count(isEven) shouldBe 2)
_ <- IO(seconds.count(!isEven(_)) shouldBe 1)
} yield ()).unsafeRunSync()
_ = assertEquals(seconds.count(isEven), 2)
_ = assertEquals(seconds.count(!isEven(_)), 1)
} yield ()
}

test("timezones") {
Expand All @@ -66,6 +65,6 @@ class Cron4sSchedulerTest extends AnyFunSuite with Matchers {

val s1 = scheduler.awakeEvery(evenSeconds) >> evalInstantNow
val s2 = s1.map(instantSeconds).take(2).forall(!isEven(_))
s2.compile.last.map(_ should be(Option(true))).unsafeRunSync()
assertIO(s2.compile.last, Some(true))
}
}
5 changes: 3 additions & 2 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import sbt._

object Dependencies {
val calevCore = "com.github.eikek" %% "calev-core" % "0.6.4"
val cron4s = "com.github.alonsodomin.cron4s" %% "cron4s-core" % "0.6.1"
val cronUtils = "com.cronutils" % "cron-utils" % "9.2.0"
val fs2Core = "co.fs2" %% "fs2-core" % "3.6.1"
val scalaTest = "org.scalatest" %% "scalatest" % "3.2.15"
val calevCore = "com.github.eikek" %% "calev-core" % "0.6.4"
val munitCatsEffect = "org.typelevel" %% "munit-cats-effect-3" % "1.0.7"
}