Compare commits

..

4 commits
0.1.1 ... main

Author SHA1 Message Date
2982d729bf (minor) Version updates, ScalaDoc, bug fixes. (#6)
All checks were successful
/ Build and Release Library (push) Successful in 1m59s
Reviewed-on: #6
2025-03-23 20:02:08 +00:00
32a69d734b (minor) Maintenance and using GS UUID (#5)
All checks were successful
/ Build and Release Library (push) Successful in 2m0s
Reviewed-on: #5
2024-08-03 16:44:12 +00:00
8e01e9d6c8 (patch) Update readme, force build for prior merge. (#4)
All checks were successful
/ Build and Release Library (push) Successful in 1m51s
Reviewed-on: #4
2024-06-24 02:44:52 +00:00
40fe036ce4 Adding support for boolean and optional values. Updating base versions. (#3)
All checks were successful
/ Build and Release Library (push) Successful in 1m19s
Reviewed-on: #3
2024-06-24 02:35:39 +00:00
11 changed files with 441 additions and 46 deletions

View file

@ -1,5 +1,5 @@
// See: https://github.com/scalameta/scalafmt/tags for the latest tags.
version = 3.8.1
version = 3.8.2
runner.dialect = scala3
maxColumn = 80

View file

@ -7,14 +7,18 @@ Test data generation library for Scala 3. `gs-datagen` provides _composable_
generator definitions that can be reused across tests.
- [Usage](#usage)
- [Dependency](#dependency)
- [Imports](#imports)
- [Examples](#examples)
- [Example: Generate a Random User](#example-generate-a-random-user)
- [Example: Require Input](#example-require-input)
- [Supported Generators](#supported-generators)
- [Donate](#donate)
## Usage
### Dependency
```
object Gs {
val Datagen: ModuleID =
@ -28,7 +32,7 @@ The standard way to use `gs-datagen` is to import the entire package, which
pulls in `Gen[A]`, `Generated[A]` and `Datagen[A, -I]`:
```scala
import gs.datagen.v0.*
import gs.datagen.v0._
```
## Examples

View file

@ -1,4 +1,4 @@
val scala3: String = "3.4.1"
val scala3: String = "3.6.4"
ThisBuild / scalaVersion := scala3
ThisBuild / versionScheme := Some("semver-spec")
@ -22,7 +22,7 @@ val sharedSettings = Seq(
lazy val testSettings = Seq(
libraryDependencies ++= Seq(
"org.scalameta" %% "munit" % "1.0.0-M12" % Test
"org.scalameta" %% "munit" % "1.1.0" % Test
)
)
@ -39,6 +39,6 @@ lazy val core = project
.settings(name := s"${gsProjectName.value}-core-v${semVerMajor.value}")
.settings(
libraryDependencies ++= Seq(
"gs" %% "gs-uuid-v0" % "0.2.3"
"gs" %% "gs-uuid-v0" % "0.4.0"
)
)

View file

@ -7,11 +7,23 @@ trait Generated[A]:
object Generated:
/** Summon an instance of the [[Generated]] type class.
*
* @param G
* The type class instance.
* @return
* The summoned instance of [[Generated]].
*/
def apply[A](
using
G: Generated[A]
): Generated[A] = G
/** Implementation of [[Generated]] based on an instance of [[Gen]].
*
* @param gen
* The underlying [[Gen]] used to produce values.
*/
final class FromGenerator[A](gen: Gen[A]) extends Generated[A]:
override def generate(): A = gen.gen()

View file

@ -1,6 +1,7 @@
package gs.datagen.v0
import gs.datagen.v0.generators.Alphabet
import gs.datagen.v0.generators.GenBool
import gs.datagen.v0.generators.GenFiniteDuration
import gs.datagen.v0.generators.GenInteger
import gs.datagen.v0.generators.GenList
@ -9,16 +10,17 @@ import gs.datagen.v0.generators.GenLong
import gs.datagen.v0.generators.GenMap
import gs.datagen.v0.generators.GenOneOf
import gs.datagen.v0.generators.GenOneOfGen
import gs.datagen.v0.generators.GenOption
import gs.datagen.v0.generators.GenSet
import gs.datagen.v0.generators.GenString
import gs.datagen.v0.generators.GenUUID
import gs.datagen.v0.generators.MinMax
import gs.datagen.v0.generators.Size
import gs.uuid.v0.UUID
import java.time.Clock
import java.time.Instant
import java.time.LocalDate
import java.util.Random
import java.util.UUID
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference
import scala.concurrent.duration.DAYS
@ -88,11 +90,66 @@ object Gen:
*/
def single[A](value: A): Gen[A] = Datagen.pure(value)
/** Generator for a list of some [[Size]] based on a generator for the list
* elements.
/** Generator which produces Boolean values.
*
* @return
* New generator that randomly produces true/false.
*/
def boolean(): Gen[Boolean] = new GenBool()
object option:
/** Generator that will randomly determine whether `Some` or `None` is
* returned for the given underlying generator.
*
* @param generator
* The generator that populates the content.
* @return
* The generator for the optional content.
*/
def apply[A](generator: Gen[A]): Gen[Option[A]] =
GenOption[A](generator)
/** Generator that will randomly determine whether `Some` or `None` is
* returned for the given underlying generator.
*
* @param generator
* The generator that populates the content.
* @return
* The generator for the optional content.
*/
def random[A](generator: Gen[A]): Gen[Option[A]] =
apply(generator)
/** Generator that will always return `Some`, using some underlying
* generator.
*
* @param generator
* The generator that populates the content.
* @return
* The generator for the optional content.
*/
def alwaysSome[A](generator: Gen[A]): GenOption[A] =
GenOption.alwaysSome(generator)
/** Generator that will always return `None`, and therefore will never
* actually invoke the underlying generator.
*
* @param generator
* The generator that populates the content.
* @return
* The generator for the optional content.
*/
def alwaysNone[A](generator: Gen[A]): GenOption[A] =
GenOption.alwaysNone(generator)
end option
/** Generator for a list of some [[gs.datagen.v0.generators.Size]] based on a
* generator for the list elements.
*
* @param size
* The [[Size]] of the list.
* The [[gs.datagen.v0.generators.Size]] of the list.
* @param gen
* The generator for the list elements.
*/
@ -101,12 +158,12 @@ object Gen:
gen: Gen[A]
): Gen[List[A]] = new GenList[A](size, gen)
/** Generator for a set of some [[Size]] based on a generator for the set
* elements.
/** Generator for a set of some [[gs.datagen.v0.generators.Size]] based on a
* generator for the set elements.
*
* @param size
* The goal [[Size]] of the set. If duplicate elements are generated, the
* size will be less then specified.
* The goal [[gs.datagen.v0.generators.Size]] of the set. If duplicate
* elements are generated, the size will be less then specified.
* @param gen
* The generator for the list elements.
*/
@ -177,7 +234,7 @@ object Gen:
* values.
*
* @param size
* The [[Size]] of the generated map.
* The [[gs.datagen.v0.generators.Size]] of the generated map.
* @param keyGen
* The generator for keys.
* @param valueGen
@ -198,7 +255,7 @@ object Gen:
* keys to values.
*
* @param size
* The [[Size]] of the generated map.
* The [[gs.datagen.v0.generators.Size]] of the generated map.
* @param keyValueGen
* The generator for key/value pairs.
*/
@ -217,8 +274,9 @@ object Gen:
*/
object string:
/** Generator for a string of the specified [[Size]], where the characters
* are restricted by the given alphabet.
/** Generator for a string of the specified
* [[gs.datagen.v0.generators.Size]], where the characters are restricted
* by the given alphabet.
*
* @param size
* The size constraints for the generated string.
@ -234,8 +292,9 @@ object Gen:
size = size
)
/** Generator for a string of the specified [[Size]], where the characters
* are restricted to ASCII lowercase letters.
/** Generator for a string of the specified
* [[gs.datagen.v0.generators.Size]], where the characters are restricted
* to ASCII lowercase letters.
*
* @param size
* The size constraints for the generated string.
@ -248,8 +307,9 @@ object Gen:
size = size
)
/** Generator for a string of the specified [[Size]], where the characters
* are restricted to ASCII uppercase letters.
/** Generator for a string of the specified
* [[gs.datagen.v0.generators.Size]], where the characters are restricted
* to ASCII uppercase letters.
*
* @param size
* The size constraints for the generated string.
@ -262,8 +322,9 @@ object Gen:
size = size
)
/** Generator for a string of the specified [[Size]], where the characters
* are restricted to ASCII letters.
/** Generator for a string of the specified
* [[gs.datagen.v0.generators.Size]], where the characters are restricted
* to ASCII letters.
*
* @param size
* The size constraints for the generated string.
@ -276,8 +337,9 @@ object Gen:
size = size
)
/** Generator for a string of the specified [[Size]], where the characters
* are restricted to ASCII letters and numbers.
/** Generator for a string of the specified
* [[gs.datagen.v0.generators.Size]], where the characters are restricted
* to ASCII letters and numbers.
*
* @param size
* The size constraints for the generated string.
@ -290,8 +352,9 @@ object Gen:
size = size
)
/** Generator for a string of the specified [[Size]], where the characters
* are restricted to lowercase ASCII letters and numbers.
/** Generator for a string of the specified
* [[gs.datagen.v0.generators.Size]], where the characters are restricted
* to lowercase ASCII letters and numbers.
*
* @param size
* The size constraints for the generated string.
@ -304,8 +367,9 @@ object Gen:
size = size
)
/** Generator for a string of the specified [[Size]], where the characters
* are restricted to uppercase ASCII letters and numbers.
/** Generator for a string of the specified
* [[gs.datagen.v0.generators.Size]], where the characters are restricted
* to uppercase ASCII letters and numbers.
*
* @param size
* The size constraints for the generated string.
@ -320,7 +384,7 @@ object Gen:
end string
/** Generators for integers.
/** Generators for integers (32-bit integers).
*/
object integer:
@ -354,7 +418,7 @@ object Gen:
end integer
/** Generators for longs.
/** Generators for longs (64-bit integers).
*/
object long:
@ -396,7 +460,7 @@ object Gen:
*
* ## Example
*
* The following generator produces a date before today where:
* The following generator produces a date before the given date where:
*
* - The day component is subtracted by 0 to 10.
* - The month component is subtracted by 1 to 11.
@ -437,6 +501,37 @@ object Gen:
years = years
)
/** Generate a date before the current date.
*
* ## Example
*
* The following generator produces a date before **today** where:
*
* - The day component is subtracted by 0 to 10.
* - The month component is subtracted by 1 to 11.
* - The year component is subtracted by 0 to 30.
*
* The produced date will be, at most, 30 years, 11 months and 10 days
* prior to the selected pivot. It will be at least 1 month prior to the
* selected pivot.
*
* {{{
* Gen.date.beforeToday(
* days = MinMax.nonNegative(0, 10),
* months = MinMax.nonNegative(1, 11),
* years = MinMax.nonNegative(0, 30)
* )
* }}}
*
* @param date
* The maximum date.
* @param days
* The range of days prior to today.
* @param months
* The range of months prior to today.
* @param years
* The range of years prior to today.
*/
def beforeToday(
days: MinMax.NonNegative,
months: MinMax.NonNegative = MinMax.Zero,
@ -450,6 +545,38 @@ object Gen:
years = years
)
/** Generate a date after the given date.
*
* ## Example
*
* The following generator produces a date after the given date where:
*
* - The day component is added by 0 to 10.
* - The month component is added by 1 to 11.
* - The year component is added by 0 to 30.
*
* The produced date will be, at most, 30 years, 11 months and 10 days
* after the selected pivot. It will be at least 1 month after the selected
* pivot.
*
* {{{
* Gen.date.after(
* date = LocalDate.now(),
* days = MinMax.nonNegative(0, 10),
* months = MinMax.nonNegative(1, 11),
* years = MinMax.nonNegative(0, 30)
* )
* }}}
*
* @param date
* The maximum date.
* @param days
* The range of days after the given date.
* @param months
* The range of months after the given date.
* @param years
* The range of years after the given date.
*/
def after(
date: LocalDate,
days: MinMax.NonNegative,
@ -463,6 +590,38 @@ object Gen:
years = years
)
/** Generate a date after the current date.
*
* ## Example
*
* The following generator produces a date after the **current** date
* where:
*
* - The day component is added by 0 to 10.
* - The month component is added by 1 to 11.
* - The year component is added by 0 to 30.
*
* The produced date will be, at most, 30 years, 11 months and 10 days
* after the selected pivot. It will be at least 1 month after the selected
* pivot.
*
* {{{
* Gen.date.afterToday(
* days = MinMax.nonNegative(0, 10),
* months = MinMax.nonNegative(1, 11),
* years = MinMax.nonNegative(0, 30)
* )
* }}}
*
* @param date
* The maximum date.
* @param days
* The range of days after today.
* @param months
* The range of months after today.
* @param years
* The range of years after today.
*/
def afterToday(
days: MinMax.NonNegative,
months: MinMax.NonNegative = MinMax.Zero,
@ -476,26 +635,89 @@ object Gen:
years = years
)
/** Generate a date around (centered upon) the given date.
*
* ## Example
*
* The following generator produces a date around the given date where:
*
* - The day component is added or subtracted by 0 to 10.
* - The month component is added or subtracted by 1 to 11.
* - The year component is added or subtracted by 0 to 30.
*
* The produced date will be, at most, 30 years, 11 months and 10 days
* before or after the selected pivot. It will be at least 1 month before
* or after the selected pivot.
*
* {{{
* Gen.date.around(
* date = LocalDate.now(),
* days = MinMax.nonNegative(0, 10),
* months = MinMax.nonNegative(1, 11),
* years = MinMax.nonNegative(0, 30)
* )
* }}}
*
* @param date
* The maximum date.
* @param days
* The range of days before or after the given date.
* @param months
* The range of months before or after the given date.
* @param years
* The range of years before or after the given date.
*/
def around(
date: LocalDate,
days: MinMax,
months: MinMax = MinMax.Zero,
years: MinMax = MinMax.Zero
): Gen[LocalDate] =
new GenLocalDate.After(
new GenLocalDate.Around(
pivot = date,
days = days,
months = months,
years = years
)
/** Generate a date around (centered upon) today.
*
* ## Example
*
* The following generator produces a date around today where:
*
* - The day component is added or subtracted by 0 to 10.
* - The month component is added or subtracted by 1 to 11.
* - The year component is added or subtracted by 0 to 30.
*
* The produced date will be, at most, 30 years, 11 months and 10 days
* before or after today. It will be at least 1 month before or after
* today.
*
* {{{
* Gen.date.around(
* days = MinMax.nonNegative(0, 10),
* months = MinMax.nonNegative(1, 11),
* years = MinMax.nonNegative(0, 30)
* )
* }}}
*
* @param date
* The maximum date.
* @param days
* The range of days before or after today.
* @param months
* The range of months before or after today.
* @param years
* The range of years before or after today.
*/
def aroundToday(
days: MinMax,
months: MinMax = MinMax.Zero,
years: MinMax = MinMax.Zero,
clock: Clock = DefaultClock
): Gen[LocalDate] =
new GenLocalDate.After(
new GenLocalDate.Around(
pivot = LocalDate.now(clock),
days = days,
months = months,
@ -504,7 +726,7 @@ object Gen:
end date
/** Geneators for `java.util.UUID` values.
/** Geneators for GS `UUID` values.
*/
object uuid:
@ -514,12 +736,7 @@ object Gen:
/** Generator for UUIDs represented as strings.
*/
def string(): Gen[String] = uuid.random().map(id => id.toString)
/** Generator for UUIDs represented as strings, without dashes.
*/
def noDashes(): Gen[String] =
uuid.random().map(id => id.toString.replace("-", ""))
def string(): Gen[String] = uuid.random().map(id => id.str())
end uuid

View file

@ -0,0 +1,8 @@
package gs.datagen.v0.generators
import gs.datagen.v0.Gen
final class GenBool() extends Gen[Boolean]:
override def generate(input: Any): Boolean =
Gen.rng().nextBoolean()

View file

@ -2,11 +2,28 @@ package gs.datagen.v0.generators
import gs.datagen.v0.Gen
import java.time.LocalDate
import java.util.Random
/** Base for generators that produce `java.time.LocalDate` values.
*/
trait GenLocalDate extends Gen[LocalDate]
/** Provids implementations for [[GenLocalDate]].
*/
object GenLocalDate:
/** Implementation of [[GenLocalDate]] that selects random dates before some
* given date.
*
* @param pivot
* The pivot date before which random values are selected.
* @param days
* The day bound.
* @param months
* The month bound.
* @param years
* The year bound.
*/
final class Before(
val pivot: LocalDate,
val days: MinMax,
@ -14,6 +31,8 @@ object GenLocalDate:
val years: MinMax
) extends GenLocalDate:
/** @inheritDocs
*/
override def generate(input: Any): LocalDate =
val rng = Gen.rng()
pivot
@ -21,6 +40,18 @@ object GenLocalDate:
.minusMonths(months.select(rng).toInt)
.minusYears(years.select(rng).toInt)
/** Implementation of [[GenLocalDate]] that selects random dates after some
* given date.
*
* @param pivot
* The pivot date after which random values are selected.
* @param days
* The day bound.
* @param months
* The month bound.
* @param years
* The year bound.
*/
final class After(
val pivot: LocalDate,
val days: MinMax,
@ -28,6 +59,8 @@ object GenLocalDate:
val years: MinMax
) extends GenLocalDate:
/** @inheritDocs
*/
override def generate(input: Any): LocalDate =
val rng = Gen.rng()
pivot
@ -35,4 +68,82 @@ object GenLocalDate:
.plusMonths(months.select(rng).toInt)
.plusYears(years.select(rng).toInt)
/** Implementation of [[GenLocalDate]] that selects random dates centered on
* some given date.
*
* @param pivot
* The pivot date around which random values are selected.
* @param days
* The day bound.
* @param months
* The month bound.
* @param years
* The year bound.
*/
final class Around(
val pivot: LocalDate,
val days: MinMax,
val months: MinMax,
val years: MinMax
) extends GenLocalDate:
/** @inheritDocs
*/
override def generate(input: Any): LocalDate =
val rng = Gen.rng()
pivot
.plusOrMinusDays(rng, days)
.plusOrMinusMonths(rng, months)
.plusOrMinusYears(rng, years)
extension (base: LocalDate)
/** Select a bounded random number of days before or after the base date.
*
* @param rng
* The random generator.
* @param range
* The bounded range.
* @return
* The new date.
*/
def plusOrMinusDays(
rng: Random,
range: MinMax
): LocalDate =
if rng.nextBoolean() then base.plusDays(range.select(rng).toInt)
else base.minusDays(range.select(rng).toInt)
/** Select a bounded random number of months before or after the base date.
*
* @param rng
* The random generator.
* @param range
* The bounded range.
* @return
* The new date.
*/
def plusOrMinusMonths(
rng: Random,
range: MinMax
): LocalDate =
if rng.nextBoolean() then base.plusMonths(range.select(rng).toInt)
else base.minusMonths(range.select(rng).toInt)
/** Select a bounded random number of years before or after the base date.
*
* @param rng
* The random generator.
* @param range
* The bounded range.
* @return
* The new date.
*/
def plusOrMinusYears(
rng: Random,
range: MinMax
): LocalDate =
if rng.nextBoolean() then base.plusYears(range.select(rng).toInt)
else base.minusYears(range.select(rng).toInt)
end GenLocalDate

View file

@ -0,0 +1,42 @@
package gs.datagen.v0.generators
import GenOption.DeterminationType
import gs.datagen.v0.Gen
final class GenOption[A](
val determinationType: DeterminationType,
val generator: Gen[A]
) extends Gen[Option[A]]:
override def generate(input: Any): Option[A] =
determinationType match
case DeterminationType.AlwaysSome =>
Some(generator.gen())
case DeterminationType.AlwaysNone =>
None
case DeterminationType.Random =>
if Gen.rng().nextBoolean() then Some(generator.gen()) else None
object GenOption:
def apply[A](generator: Gen[A]): GenOption[A] =
new GenOption[A](DeterminationType.Random, generator)
def random[A](generator: Gen[A]): GenOption[A] = apply[A](generator)
def alwaysSome[A](generator: Gen[A]): GenOption[A] =
new GenOption[A](DeterminationType.AlwaysSome, generator)
def alwaysNone[A](generator: Gen[A]): GenOption[A] =
new GenOption[A](DeterminationType.AlwaysNone, generator)
enum DeterminationType:
case AlwaysSome, AlwaysNone, Random
object DeterminationType:
given CanEqual[DeterminationType, DeterminationType] = CanEqual.derived
end DeterminationType
end GenOption

View file

@ -1,9 +1,10 @@
package gs.datagen.v0.generators
import gs.datagen.v0.Gen
import java.util.UUID
import gs.uuid.v0.UUID
final class GenUUID extends Gen[UUID]:
given UUID.Generator = UUID.Generator.version4
override def generate(input: Any): UUID =
UUID.randomUUID()
UUID.generate()

View file

@ -1 +1 @@
sbt.version=1.9.9
sbt.version=1.10.11

View file

@ -28,6 +28,6 @@ externalResolvers := Seq(
"Garrity Software Releases" at "https://maven.garrity.co/gs"
)
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.11")
addSbtPlugin("gs" % "sbt-garrity-software" % "0.3.0")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.1")
addSbtPlugin("gs" % "sbt-garrity-software" % "0.5.0")
addSbtPlugin("gs" % "sbt-gs-semver" % "0.3.0")