Skip to content

Commit

Permalink
refactor: introduce a parser module that provides and ADT for parsers
Browse files Browse the repository at this point in the history
  • Loading branch information
mbaechler committed Dec 19, 2024
1 parent 9e2cdf0 commit 737bdc5
Show file tree
Hide file tree
Showing 75 changed files with 433 additions and 260 deletions.
4 changes: 2 additions & 2 deletions bench/src/main/scala/cron4s/bench/ParserBenchmark.scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ class ParserBenchmark {
var cronString: String = _

@Benchmark
def parserCombinators() = parsing.parse(cronString)
def parserCombinators() = parsing.Parser.parse(cronString)

@Benchmark
def attoParser() = atto.parser(cronString)
def attoParser() = atto.Parser.parse(cronString)
}
20 changes: 10 additions & 10 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -279,10 +279,10 @@ lazy val cron4sJS = (project in file(".js"))
.settings(commonJsSettings: _*)
.settings(noPublishSettings)
.enablePlugins(ScalaJSPlugin)
.aggregate(core.js, kernel.js, atto.js, momentjs, circe.js, decline.js, testkit.js, tests.js)
.aggregate(core.js, parser.js, atto.js, momentjs, circe.js, decline.js, testkit.js, tests.js)
.dependsOn(
core.js,
kernel.js,
parser.js,
atto.js,
momentjs,
circe.js,
Expand All @@ -301,7 +301,7 @@ lazy val cron4sJVM = (project in file(".jvm"))
.settings(consoleSettings)
.settings(noPublishSettings)
.aggregate(
kernel.jvm,
parser.jvm,
core.jvm,
atto.jvm,
joda,
Expand Down Expand Up @@ -357,11 +357,11 @@ lazy val docs = project
// Main modules
// =================================================================================

lazy val kernel = (crossProject(JSPlatform, JVMPlatform, NativePlatform) in file("modules/kernel"))
lazy val parser = (crossProject(JSPlatform, JVMPlatform, NativePlatform) in file("modules/parser"))
.enablePlugins(AutomateHeaderPlugin, ScalafmtPlugin, MimaPlugin)
.settings(
name := "kernel",
moduleName := "cron4s-kernel"
name := "parser",
moduleName := "cron4s-parser"
)
.settings(commonSettings)
.settings(publishSettings)
Expand All @@ -371,7 +371,7 @@ lazy val kernel = (crossProject(JSPlatform, JVMPlatform, NativePlatform) in file
.jvmSettings(commonJvmSettings)
.jvmSettings(consoleSettings)
.jvmSettings(Dependencies.coreJVM)
.jvmSettings(mimaSettings("kernel"))
.jvmSettings(mimaSettings("parser"))
.nativeSettings(Dependencies.coreNative)

lazy val core = (crossProject(JSPlatform, JVMPlatform, NativePlatform) in file("modules/core"))
Expand All @@ -393,7 +393,7 @@ lazy val core = (crossProject(JSPlatform, JVMPlatform, NativePlatform) in file("
.jvmConfigure(_.dependsOn(atto.jvm))
.nativeSettings(Dependencies.coreNative)
.nativeConfigure(_.dependsOn(parserc.native))
.dependsOn(kernel)
.dependsOn(parser)

lazy val parserc =
(crossProject(JSPlatform, JVMPlatform, NativePlatform) in file("modules/parserc"))
Expand All @@ -410,7 +410,7 @@ lazy val parserc =
.jvmSettings(commonJvmSettings)
.jvmSettings(Dependencies.coreJVM)
.nativeSettings(Dependencies.coreNative)
.dependsOn(kernel)
.dependsOn(parser)

lazy val atto = (crossProject(JSPlatform, JVMPlatform) in file("modules/atto"))
.enablePlugins(AutomateHeaderPlugin, ScalafmtPlugin, MimaPlugin)
Expand All @@ -425,7 +425,7 @@ lazy val atto = (crossProject(JSPlatform, JVMPlatform) in file("modules/atto"))
.jsSettings(Dependencies.coreJS)
.jvmSettings(commonJvmSettings)
.jvmSettings(Dependencies.coreJVM)
.dependsOn(kernel)
.dependsOn(parser)

lazy val testkit =
(crossProject(JSPlatform, JVMPlatform, NativePlatform) in file("modules/testkit"))
Expand Down
115 changes: 47 additions & 68 deletions modules/atto/shared/src/main/scala/cron4s/atto/Parser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,11 @@ package cron4s.atto
import _root_.atto.{Parser => AttoParser, _}
import atto.Atto._
import cats.implicits._
import cron4s.CronField._
import cron4s.CronUnit._
import cron4s._
import cron4s.expr._

private object Parser {
object Parser extends cron4s.parser.Parser {

import cron4s.parser._
import cron4s.parser.Node._

private def oneOrTwoDigitsPositiveInt: AttoParser[Int] = {

Expand Down Expand Up @@ -60,122 +59,102 @@ private object Parser {
// ----------------------------------------

// Seconds

val seconds: AttoParser[ConstNode[Second]] =
sexagesimal.map(ConstNode[Second](_))
private val seconds: AttoParser[ConstNode] = sexagesimal.map(ConstNode(_))

// Minutes

val minutes: AttoParser[ConstNode[Minute]] =
sexagesimal.map(ConstNode[Minute](_))
private val minutes: AttoParser[ConstNode] = sexagesimal.map(ConstNode(_))

// Hours

val hours: AttoParser[ConstNode[Hour]] =
oneOrTwoDigitsPositiveInt.filter(x => (x >= 0) && (x < 24)).map(ConstNode[Hour](_))
private val hours: AttoParser[ConstNode] =
oneOrTwoDigitsPositiveInt.filter(x => (x >= 0) && (x < 24)).map(ConstNode(_))

// Days Of Month

val daysOfMonth: AttoParser[ConstNode[DayOfMonth]] =
oneOrTwoDigitsPositiveInt.filter(x => (x >= 1) && (x <= 31)).map(ConstNode[DayOfMonth](_))
private val daysOfMonth: AttoParser[ConstNode] =
oneOrTwoDigitsPositiveInt.filter(x => (x >= 1) && (x <= 31)).map(ConstNode(_))

// Months

private[this] val numericMonths =
oneOrTwoDigitsPositiveInt.filter(x => (x >= 0) && (x <= 12)).map(ConstNode[Month](_))
private[this] val numericMonths: AttoParser[ConstNode] =
oneOrTwoDigitsPositiveInt.filter(x => (x >= 0) && (x <= 12)).map(ConstNode(_))

private[this] val textualMonths =
private[this] val textualMonths: AttoParser[ConstNode] =
literal.filter(Months.textValues.contains).map { value =>
val index = Months.textValues.indexOf(value)
ConstNode[Month](index + 1, Some(value))
ConstNode(index + 1, Some(value))
}

val months: AttoParser[ConstNode[Month]] =
private val months: AttoParser[ConstNode] =
textualMonths | numericMonths

// Days Of Week

private[this] val numericDaysOfWeek =
oneOrTwoDigitsPositiveInt.filter(x => (x >= 0) && (x <= 6)).map(ConstNode[DayOfWeek](_))
private[this] val numericDaysOfWeek: AttoParser[ConstNode] =
oneOrTwoDigitsPositiveInt.filter(x => (x >= 0) && (x <= 6)).map(ConstNode(_))

private[this] val textualDaysOfWeek =
private[this] val textualDaysOfWeek: AttoParser[ConstNode] =
literal.filter(DaysOfWeek.textValues.contains).map { value =>
val index = DaysOfWeek.textValues.indexOf(value)
ConstNode[DayOfWeek](index, Some(value))
ConstNode(index, Some(value))
}

val daysOfWeek: AttoParser[ConstNode[DayOfWeek]] =
private val daysOfWeek: AttoParser[ConstNode] =
textualDaysOfWeek | numericDaysOfWeek

// ----------------------------------------
// Field-Based Expression Atoms
// ----------------------------------------

def each[F <: CronField](implicit unit: CronUnit[F]): AttoParser[EachNode[F]] =
asterisk.as(EachNode[F])
private def each: AttoParser[EachNode.type] = asterisk.as(EachNode)

def any[F <: CronField](implicit unit: CronUnit[F]): AttoParser[AnyNode[F]] =
questionMark.as(AnyNode[F])
private def any: AttoParser[AnyNode.type] = questionMark.as(AnyNode)

def between[F <: CronField](base: AttoParser[ConstNode[F]])(implicit
unit: CronUnit[F]
): AttoParser[BetweenNode[F]] =
private def between(base: AttoParser[ConstNode]): AttoParser[BetweenNode] =
for {
min <- base <~ hyphen
max <- base
} yield BetweenNode[F](min, max)
} yield BetweenNode(min, max)

def several[F <: CronField](base: AttoParser[ConstNode[F]])(implicit
unit: CronUnit[F]
): AttoParser[SeveralNode[F]] = {
def compose(b: => AttoParser[EnumerableNode[F]]) =
private def several(base: AttoParser[ConstNode]): AttoParser[SeveralNode] = {
def compose(b: => AttoParser[EnumerableNode]) =
sepBy(b, comma)
.collect {
case first :: second :: tail => SeveralNode(first, second, tail: _*)
}

compose(between(base).map(between2Enumerable) | base.map(const2Enumerable))
compose(between(base) | base)
}

def every[F <: CronField](base: AttoParser[ConstNode[F]])(implicit
unit: CronUnit[F]
): AttoParser[EveryNode[F]] = {
def compose(b: => AttoParser[DivisibleNode[F]]) =
private def every(base: AttoParser[ConstNode]): AttoParser[EveryNode] = {
def compose(b: => AttoParser[DivisibleNode]) =
((b <~ slash) ~ oneOrTwoDigitsPositiveInt.filter(_ > 0)).map {
case (exp, freq) => EveryNode[F](exp, freq)
case (exp, freq) => EveryNode(exp, freq)
}

compose(
several(base).map(several2Divisible) |
between(base).map(between2Divisible) |
each[F].map(each2Divisible)
)
compose(several(base) | between(base) | each)
}

// ----------------------------------------
// AST Parsing & Building
// ----------------------------------------

def field[F <: CronField](base: AttoParser[ConstNode[F]])(implicit
unit: CronUnit[F]
): AttoParser[FieldNode[F]] =
every(base).map(every2Field) |
several(base).map(several2Field) |
between(base).map(between2Field) |
base.map(const2Field) |
each[F].map(each2Field)

def fieldWithAny[F <: CronField](base: AttoParser[ConstNode[F]])(implicit
unit: CronUnit[F]
): AttoParser[FieldNodeWithAny[F]] =
every(base).map(every2FieldWithAny) |
several(base).map(several2FieldWithAny) |
between(base).map(between2FieldWithAny) |
base.map(const2FieldWithAny) |
each[F].map(each2FieldWithAny) |
any[F].map(any2FieldWithAny)

val cron: AttoParser[CronExpr] = for {
private def field(base: AttoParser[ConstNode]): AttoParser[NodeWithoutAny] =
every(base) |
several(base) |
between(base) |
base |
each

private def fieldWithAny(base: AttoParser[ConstNode]): AttoParser[Node] =
every(base) |
several(base) |
between(base) |
base |
each |
any

private val cron: AttoParser[CronExpr] = for {
sec <- field(seconds) <~ blank
min <- field(minutes) <~ blank
hour <- field(hours) <~ blank
Expand Down
23 changes: 0 additions & 23 deletions modules/atto/shared/src/main/scala/cron4s/atto/package.scala

This file was deleted.

2 changes: 1 addition & 1 deletion modules/core/js/src/main/scala/cron4s/Cron.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ import scala.scalajs.js.annotation.JSExportTopLevel
* @author Antonio Alonso Dominguez
*/
@JSExportTopLevel("Cron")
object Cron extends CronImpl(atto.parser)
object Cron extends CronImpl(atto.Parser)
2 changes: 1 addition & 1 deletion modules/core/jvm/src/main/scala/cron4s/Cron.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ import scala.scalajs.js.annotation.JSExportTopLevel
* @author Antonio Alonso Dominguez
*/
@JSExportTopLevel("Cron")
object Cron extends CronImpl(atto.parser)
object Cron extends CronImpl(atto.Parser)
2 changes: 1 addition & 1 deletion modules/core/native/src/main/scala/cron4s/Cron.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ import scala.scalajs.js.annotation.JSExportTopLevel
* @author Antonio Alonso Dominguez
*/
@JSExportTopLevel("Cron")
object Cron extends CronImpl(parsing.parse)
object Cron extends CronImpl(parsing.Parser)
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package cron4s

import cron4s.parser.Parser

import scala.util.Try

private[cron4s] class CronImpl(parser: Parser) {
Expand All @@ -40,7 +42,7 @@ private[cron4s] class CronImpl(parser: Parser) {
*/
@inline
def parse(e: String): Either[Error, CronExpr] =
parser(e).flatMap(validation.validateCron)
ParserAdapter.adapt(parser)(e).flatMap(validation.validateCron)

/**
* Parses the given cron expression into a cron AST using Try as return type
Expand Down
Loading

0 comments on commit 737bdc5

Please sign in to comment.