From a8580fce9f3cb60c28b01b935c20d91b6c13de1a Mon Sep 17 00:00:00 2001 From: Flavian Alexandru Date: Fri, 9 Nov 2018 01:21:51 +0000 Subject: [PATCH 1/2] Set theme jekyll-theme-cayman --- _config.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 _config.yml diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..c419263 --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-cayman \ No newline at end of file From 39add2402fbf417e23838fcf07215d83139a8885 Mon Sep 17 00:00:00 2001 From: Flavian Alexandru Date: Fri, 21 Dec 2018 02:34:29 +0000 Subject: [PATCH 2/2] Merging to auto-publish GH pages [ci skip] (#76) * Feature/empty samplers (#73) * Renaming methods to ensure scalatest compat * Adding membership * Adding more methods. * Trying to fix overloading issues with empty * Removing commented code * Removing known field dictionary * Removing known field dictionary * Removing println and adding one for tests * Fixing test references. * Remove comments * Setting version to 0.46.0 [ci skip] * Feature/add fill options (#74) * Setting version to 0.46.0 [ci skip] * Adding compilation flag to alwawys fill options * Adding test for filled options. * Manually resetting version * Setting version to 0.46.1 [ci skip] * Setting version to 0.47.0-SNAPSHOT [ci skip] * Feature/tut docs (#75) * Adding tut plugin * Add in readme for the project * Updating the documentation to use tut * Adding examples of how to derive new types. * Simplifying the logic with env variables * Removing assertions test as assertions have been removed. * Updating patience configuration * Adding environment exports * Trying to fix Travis logic * Adding docs folder output * Changing entrypoint into readme * Setting version to 0.47.0 [ci skip] * Setting version to 0.48.0-SNAPSHOT [ci skip] --- .travis.yml | 5 +- README.md | 335 +-------------- build.sbt | 26 +- build/publish_develop.sh | 4 +- build/run_tests.sh | 36 +- docs/README.md | 390 ++++++++++++++++++ project/plugins.sbt | 1 + readme/src/main/tut/README.md | 191 +++++---- .../util/play/PlayAugmenterTests.scala | 2 +- .../com/outworkers/util/empty/Empty.scala | 82 ++++ .../util/empty/EmptyGenerators.scala | 38 ++ .../outworkers/util/empty/EmptyMacro.scala | 274 ++++++++++++ .../com/outworkers/util/empty/package.scala | 13 + .../outworkers/util/samplers/Generators.scala | 5 + .../com/outworkers/util/samplers/Optins.scala | 18 + .../com/outworkers/util/samplers/Sample.scala | 6 +- .../util/samplers/SamplerMacro.scala | 35 +- .../util/empty/EmptyGeneratorsTest.scala | 83 ++++ .../util/samplers/GeneratorsTest.scala | 17 +- .../util/samplers/Memebership.scala | 19 - .../util/samplers/TracerTests.scala | 14 +- .../outworkers/util/samplers/samples.scala | 56 --- .../org/outworkers/domain/test/domain.scala | 67 +++ .../util/testing/twitter/package.scala | 5 +- .../com/outworkers/util/testing/package.scala | 61 +-- version.sbt | 2 +- 26 files changed, 1199 insertions(+), 586 deletions(-) create mode 100644 docs/README.md create mode 100644 util-samplers/src/main/scala/com/outworkers/util/empty/Empty.scala create mode 100644 util-samplers/src/main/scala/com/outworkers/util/empty/EmptyGenerators.scala create mode 100644 util-samplers/src/main/scala/com/outworkers/util/empty/EmptyMacro.scala create mode 100644 util-samplers/src/main/scala/com/outworkers/util/empty/package.scala create mode 100644 util-samplers/src/main/scala/com/outworkers/util/samplers/Optins.scala create mode 100644 util-samplers/src/test/scala/com/outworkers/util/empty/EmptyGeneratorsTest.scala delete mode 100644 util-samplers/src/test/scala/com/outworkers/util/samplers/Memebership.scala delete mode 100644 util-samplers/src/test/scala/com/outworkers/util/samplers/samples.scala diff --git a/.travis.yml b/.travis.yml index 487aaf9..a79ad25 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,11 +17,14 @@ branches: - develop matrix: include: - - scala: 2.12.6 + - scala: 2.12.7 jdk: oraclejdk8 + env: RUN_WITH_COVERAGE=true PUBLISH_ARTIFACT=true env: global: + - RUN_WITH_COVERAGE: false + - PUBLISH_ARTIFACT: false - GH_REF: github.com/outworkers/util.git - secure: Onl6jQhDgCHVhsxIhC2FltwTlvTWI5815lI9wsb79OvE+Xl/hh8XcafOBzUJ/LtKmt021oieOsR53RAdIJDKhNrKo3AQYoyp3rAX48zCInE0Y29slKVCwj51w5Mns+aYlPbJcHJvRNWkFIpaQ1AmBvkHfc0A0rxfoSB1lOIrtHs= - secure: k/DGy5KkvzmQNJEfazsEoD6biwkIoYC9DyjhDTMCxhXLz/mURsCtfhdWo3Uz4nhuX3qDK0N/6C6BTwl0ktVEA7eH8XZQY/dW1lLTY3jglD2U/FhAfCngbcI+ToL5kdK77Zy4LKvu4XBKXfSusI20E1gbK+Tjp1uCkNvVaAyyVv0= diff --git a/README.md b/README.md index 74df96a..8e521f0 100644 --- a/README.md +++ b/README.md @@ -7,341 +7,10 @@ latest version of `util` available. The badges are automatically updated when a ![Util](https://s3-eu-west-1.amazonaws.com/websudos/oss/logos/util.png "Outworkers Util") -### Table of contents ### +### Documentation -
    -
  1. Integrating the util library
  2. - -
  3. -

    util-parsers

    - -
  4. - -
  5. -

    util-parsers-cats

    - -
  6. - -
  7. -

    util-testing

    - -
  8. +The official documentation is found [here](./docs/README.md) -
  9. -

    util-zookeeper

    -
- - -### Integrating the util library ### -Back to top - - -The util library is designed to wrap common functionality in all our frameworks and offer it at the convenience of a dependency. Anything that will be useful - long term to a great number of people belongs in these modules, to avoid duplication and help make our devs aware they can simply use what already exists. - -The full list of available modules is: - -```scala - -libraryDependencies ++= Seq( - "com.outworkers" %% "util-lift" % Versions.util, - "com.outworkers" %% "util-domain" % Versions.util, - "com.outworkers" %% "util-parsers" % Versions.util, - "com.outworkers" %% "util-parsers-cats" % Versions.util, - "com.outworkers" %% "util-validators" % Versions.util, - "com.outworkers" %% "util-validators-cats" % Versions.util, - "com.outworkers" %% "util-play" % Versions.util, - "com.outworkers" %% "util-urls" % Versions.util, - "com.outworkers" %% "util-testing" % Versions.util -) -``` - - -### util-testing ### -Back to top - -The testing module features the ```AsyncAssertionsHelper```, which builds on top of ScalaTest to offer simple asynchronous assertions. We use this pattern -heavily throughout the Outworkers ecosystem of projects, from internal to DSL modules and so forth. Asynchronous testing generally offers a considerable -performance gain in code. - - -### Async assertions ### -Back to top - - -The async assertions module features a dual API, so you can call the same methods on both ```scala.concurrent.Future``` and ```com.twitter.util.Future```. -The underlying mechanism will create an async ```Waiter```, that will wait for the future to complete within the given ```PatienceConfiguration```. The -awaiting is done asynchronously and the assertions are invoked and evaluated once the future in question has returned a result. - -```scala -import com.outworkers.util.testing._ - -class MyTests extends FlatSuite with Matchers { - - "The async computation" should "return 0 on completion" in { - val f: Future[Int] = .. // Pretend this is a Future just like any other future. - f.successful { - res => { - res shouldEqual 0 - } - } - } - - "This async computation" should "fail by design" in { - val f: Future[Unit] = .. - - // You don't even need to do anything more than failure at this stage. - // If the Future fails, the test will succeed, as this method is used when you "expect a failure". - // You can however perform assertions on the error returned. - f.failing { - err => { - } - } - } - - "This async computation" should "fail with a specific error" in { - val f: Future[Unit] = .. - f.failingWith[NumberFormatException] { - err => { - } - } - } - -} -``` - - -You can directly customise the ```timeout``` of all ```Waiters``` using the ScalaTest specific time span implementations and interval configurations. - - -```scala -import org.scalatest.concurrent.PatienceConfiguration -import org.scalatest.time.SpanSugar._ - -implicit val timeout: PatienceConfiguration.Timeout = timeout(20 seconds) - -``` - -Summary: - -- The dependency you need is ```"com.outworkers" %% "util-testing" % UtilVersion```. -- You have to import ```com.outworkers.util.testing._```. -- You have three main assertion methods, ```successful```, ```failing```, and ```failingWith```. -- You can configure the timeout of waiters with ```implicit val timeout: PatienceConfiguration.Timeout = timeout(20 seconds)```. -- The default timeout value is ```1 second```. - - -### Data sampling ### -Back to top - -This is a very common pattern we use in our testing and it's very easy to interchange this generation with something like ScalaCheck. The idea is very simple, you use type classes to define ways to sample a given type. -After you define such a one-time sampling type class instance, you have access to several methods that will allow you to generate test data. - -It's useful to define such typeclass instances inside package objects, as they will be "invisibly" imported in to the scope you need them to. This is often really neat, albeit potentially confusing for novice Scala users. - - -```scala - -import com.outworkers.util.testing._ - -@sample case class MyAwesomeClass(name: String, age: Int, email: String) -``` - -You may notice this pattern is already available in better libraries such as ScalaMock and we are not trying to provide an alternative to ScalaMock or compete with it in any way. Our typeclass generator approach only becomes very useful where you really care about very specific properties of the data. -For instance, you may want to get a user with a valid email address, or you may use the underlying factories to get a name that reassembles the name of a real person, and so on. - -It's also useful when you want to define specific ways in which hierarchies of classes are composed together into a sample. If generation for the sake of generation is all you care about, then ScalaMock is probably more robust. - - -### Automated sampling - -Back to top - -One interesting thing that happens when using the `@sample` annotation is that using `gen` immediately after it will basically -give you an instance of your `case class` with the fields appropiately pre-filled, and some of the basic scenarios are also name aware. - -What this means is that we try to make the data feel "real" with respect to what it should be. Let's take the below example: - -```scala -@sample case class User( - id: UUID, - firstName: String, - lastName: String, - email: String -) -``` -This is interesting and common enough. What's more interesting is the output of `gen`. - -```scala - -val user = gen[User] - -Console.println(user.trace()) - -/** -User( - id = 6be8914c-4274-40ee-83f5-334131246fd8 - firstName = Lindsey - lastName = Craft - email = rparker@hotma1l.us -) -*/ - -``` - -So as you can see, the fields have been appropriately pre-filled. The email is a valid email, and the first and last name look like first and last names. For -anything that's in the default generation domain, including dates and country codes and much more, we have the ability to produce automated -appropriate values. - -During the macro expansion phase, we check the annotation targets and try to infer the "natural" value based on the field name and type. So -if your field name is either "email" or "emailAddress" or anything similar enough, you will get an "email" back. - - -### Generating data - -There are multiple methods available, allowing you to generate more than just the type: - -- ```gen[T]```, used to generate a single instance of T. -- ```gen[X, Y]```, used to generate a tuple based on two samples. -- ```genOpt[T]```, convenience method that will give you back a ```Some[T](..)```. -- ```genList[T](limit)```, convenience method that will give you back a ```List[T]```. The numbers of items in the list is equal to the ```limit``` and has a default value of 5 if not specified. -- ```genMap[T]()```, convenience method that will give you back a ```Map[String, T]```. - - -There is also a default list of available generators for some default types, and to get to their value simply use the `value` method if the type is not a primitive. For things like ```EmailAddress```, the point of the extra class is obviously to distinguish the type during implicit resolution, but you don't need to use our abstraction at all, there will always be an easy way to get to the underlying generated primitives. - -In the case of email addresses, you can use ```gen[EmailAddress].value```, which will correctly generate a valid ```EmailAddress``` but you can work directly with a ```String```. - -- ```scala.Int``` -- ```scala.Double``` -- ```scala.Float``` -- ```scala.Long``` -- ```scala.String``` -- ```scala.math.BigDecimal``` -- ```scala.math.BigInt``` -- ```java.util.Date``` -- ```java.util.UUID``` -- ```org.joda.time.DateTime``` -- ```org.joda.time.LocalDate``` -- ```com.outworkers.util.domain.Definitions.EmailAddress(value)``` -- ```com.outworkers.util.domain.Definitions.FirstName(value)``` -- ```com.outworkers.util.domain.Definitions.LastName(value)``` -- ```com.outworkers.util.domain.Definitions.FullName(value)``` -- ```com.outworkers.util.domain.Definitions.CountryCode(value)``` -- ```com.outworkers.util.domain.Definitions.Country(value)``` -- ```com.outworkers.util.domain.Definitions.City(value)``` -- ```com.outworkers.util.domain.Definitions.ProgrammingLanguage(value)``` -- ```com.outworkers.util.domain.Definitions.LoremIpsum(value)``` - - -### util-parsers ### -Back to top - -The parser module features an easy to use and integrate set of ScalaZ Applicative based parsers, with an ```Option``` based parser variant. It allows us to -seamlessly deal with validation chains at REST API level or whenever validation is involved. Whether it's monadic composition of options or chaining of -applicative functors to obtain a "correct" chain, the parser module is designed to offer an all-you-can-eat buffet of mini parsers that can be easily -composed to suit any validation needs. - -Each parser comes in three distinct flavours, a ```ValidationNel``` parser that parsers the end type from a ```String``` and returns the type itself, -a parser that parses an end result from an ```Option[String]``` and parserOpt variant that returns an ```Option[T]``` instead of a ```ValidationNel[String, -T]```, which allows for Monadic composition, where you need to "short-circuit" evaluation and validation, instead of computing the full chain by chaining -applicatives. - -### Option parsers ### -Back to top - -The full list of optional parsers is: - -| Type | Input type | Parser Output type | -| --------------- |---------------------------| --------------------------------- | -| Int | String\|Option[String] | ValidationNel[String, Int] | -| Long | String\|Option[String] | ValidationNel[String, Long] | -| Double | String\|Option[String] | ValidationNel[String, Double] | -| Float | String\|Option[String] | ValidationNel[String, Float] | -| UUID | String\|Option[String] | ValidationNel[String, UUID] | -| Email | String\|Option[String] | ValidationNel[String, String] | -| DateTime | String\|Option[String] | ValidationNel[String, org.joda.time.DateTime] | - -Option parsers are designed for chains where you want to short-circuit and exit to result as soon a parser fails. This short-circuit behaviour is the default - ```flatMap``` behaviour of an ```Option```, as soon as an ```Option``` is ```None``` the chain breaks. Unlike applicatives, - the evaluation sequence of options will be escaped and you cannot for instance return an error for every parser that couldn't validate. Instead, - you will only get the first error in the sequence. - -An example of how to use ```Option``` parsers might be: - -```scala - -import com.outworkers.util.parsers._ - -object Test { - def optionalParsing(email: String, age: String): Unit = { - for { - validEmail <- parseOpt[EmailAddress](email) - validAge <- parseOpt[Int](age) - } yield s"This person can be reached at $validEmail and is $validAge years old" - } -} - -``` - - -### Applicative parsers ### -Back to top - -The full list of ScalaZ Validation based applicative parsers is: - -| Type | Input type | Parser Output type | -| --------------- |---------------------------| --------------------------------- | -| Int | String\|Option[String] | ValidationNel[String, Int] | -| Long | String\|Option[String] | ValidationNel[String, Long] | -| Double | String\|Option[String] | ValidationNel[String, Double] | -| Float | String\|Option[String] | ValidationNel[String, Float] | -| UUID | String\|Option[String] | ValidationNel[String, UUID] | -| Email | String\|Option[String] | ValidationNel[String, String] | -| DateTime | String\|Option[String] | ValidationNel[String, org.joda.time.DateTime] | - -To illustrate the basic usage of applicative parsers and how to chain them, have a look below. - -```scala - -import scalaz._ -import scalaz.Scalaz._ -import com.outworkers.util.parsers._ - -object Test { - - def registerUser(str: String, age: String): Unit = { - (parse[EmailAddress](str) |@| parse[Int](age)) { - (validEmail, validAge) => { - } - }.fold { - // .. - } - } - -} -``` ### Contributors Back to top diff --git a/build.sbt b/build.sbt index 4fa9ed8..fb82cd1 100644 --- a/build.sbt +++ b/build.sbt @@ -179,7 +179,8 @@ lazy val baseProjectList: Seq[ProjectReference] = Seq( testing, testingTwitter, macros, - tags + tags, + readme ) lazy val util = (project in file(".")) @@ -414,3 +415,26 @@ lazy val validators = (project in file("util-validators")) parsers, testing % Test ) + +lazy val readme = (project in file("readme")) + .settings(sharedSettings: _*) + .dependsOn( + domain, + lift, + liftCats, + parsers, + parsersCats, + validatorsCats, + validators, + samplers, + testing, + testingTwitter, + macros, + tags + ).settings( + tutSourceDirectory := sourceDirectory.value / "main" / "tut", + tutTargetDirectory := util.base / "docs", + libraryDependencies ++= Seq( + "org.scalatest" %% "scalatest" % Versions.scalatest % "tut" + ) + ).enablePlugins(TutPlugin) \ No newline at end of file diff --git a/build/publish_develop.sh b/build/publish_develop.sh index 28b1b88..8b390b8 100755 --- a/build/publish_develop.sh +++ b/build/publish_develop.sh @@ -3,7 +3,7 @@ echo "Pull request: ${TRAVIS_PULL_REQUEST}; Branch: ${TRAVIS_BRANCH}" if [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_BRANCH" == "develop" ]; then - if [ "${TRAVIS_SCALA_VERSION}" == "2.12.4" ] && [ "${TRAVIS_JDK_VERSION}" == "oraclejdk8" ]; + if [ "${PUBLISH_ARTIFACT}" == "true" ]; then echo "Setting git user email to ci@outworkers.com" @@ -75,7 +75,7 @@ then fi else - echo "Only publishing version for Scala 2.12.4 and Oracle JDK 8 to prevent multiple artifacts" + echo "Only publishing version for Scala 2.12.7 and Oracle JDK 8 to prevent multiple artifacts" fi else echo "This is either a pull request or the branch is not develop, deployment not necessary" diff --git a/build/run_tests.sh b/build/run_tests.sh index 74c9325..b28bd87 100755 --- a/build/run_tests.sh +++ b/build/run_tests.sh @@ -1,11 +1,27 @@ #!/usr/bin/env bash -if [ "${TRAVIS_SCALA_VERSION}" == "2.11.8" ] && [ "${TRAVIS_JDK_VERSION}" == "oraclejdk8" ]; -then - echo "Running tests with coverage and report submission" - sbt ++$TRAVIS_SCALA_VERSION coverage test coverageReport coverageAggregate - exit $? -else - echo "Running tests without attempting to submit coverage reports" - sbt "plz $TRAVIS_SCALA_VERSION test" - exit $? -fi \ No newline at end of file +function run_test_suite { + if [ "${RUN_WITH_COVERAGE}" == "true" ]; + then + echo "Running tests with coverage and report submission" + sbt ++$TRAVIS_SCALA_VERSION coverage test coverageReport coverageAggregate coveralls + test_exit_code=$? + + if [ ${test_exit_code} -eq "0" ]; + then + echo "Running tut compilation" + sbt ++$TRAVIS_SCALA_VERSION "project readme" "tut" + local tut_exit_code=$? + echo "Tut compilation exited with status $tut_exit_code" + exit ${tut_exit_code} + else + echo "Unable to run tut compilation, test suite failed" + exit ${test_exit_code} + fi + else + echo "Running tests without attempting to submit coverage reports" + sbt "plz $TRAVIS_SCALA_VERSION test" + exit $? + fi +} + +run_test_suite \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..20afd0e --- /dev/null +++ b/docs/README.md @@ -0,0 +1,390 @@ +# util[![Build Status](https://travis-ci.org/outworkers/util.svg?branch=develop)](https://travis-ci.org/outworkers/util) [![Coverage Status](https://coveralls.io/repos/github/outworkers/util/badge.svg?branch=develop)](https://coveralls.io/github/outworkers/util?branch=develop) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.outworkers/util-lift_2.11/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.outworkers/util-lift_2.11) [ ![Bintray](https://api.bintray.com/packages/outworkers/oss-releases/util/images/download.svg) ](https://bintray.com/outworkers/oss-releases/util-lift/_latestVersion) [![ScalaDoc](http://javadoc-badge.appspot.com/com.outworkers/util_2.11.svg?label=scaladoc)](http://javadoc-badge.appspot.com/com.outworkers/util-lift_2.11) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/outworkers/util?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +This library is available on Maven Central and on our public Bintray repository, found at: `https://dl.bintray.com/outworkers/oss-releases/`. + +It is publicly available, for both Scala 2.10.x and Scala 2.11.x. Check the badges at the top of this README for the +latest version of `util` available. The badges are automatically updated when a new version is out, this readme is not. + +![Util](https://s3-eu-west-1.amazonaws.com/websudos/oss/logos/util.png "Outworkers Util") + +### Table of contents ### + +
    +
  1. Integrating the util library
  2. + +
  3. +

    util-parsers

    + +
  4. + +
  5. +

    util-parsers-cats

    + +
  6. + +
  7. +

    util-testing

    + +
  8. + +
  9. +

    util-zookeeper

    +
+ + +### Integrating the util library ### +Back to top + + +The util library is designed to wrap common functionality in all our frameworks and offer it at the convenience of a dependency. Anything that will be useful + long term to a great number of people belongs in these modules, to avoid duplication and help make our devs aware they can simply use what already exists. + +The full list of available modules is: + +```scala + +libraryDependencies ++= Seq( + "com.outworkers" %% "util-lift" % Versions.util, + "com.outworkers" %% "util-domain" % Versions.util, + "com.outworkers" %% "util-parsers" % Versions.util, + "com.outworkers" %% "util-parsers-cats" % Versions.util, + "com.outworkers" %% "util-validators" % Versions.util, + "com.outworkers" %% "util-validators-cats" % Versions.util, + "com.outworkers" %% "util-play" % Versions.util, + "com.outworkers" %% "util-urls" % Versions.util, + "com.outworkers" %% "util-testing" % Versions.util +) +``` + + +### util-testing ### +Back to top + +The testing module features the ```AsyncAssertionsHelper```, which builds on top of ScalaTest to offer simple asynchronous assertions. We use this pattern +heavily throughout the Outworkers ecosystem of projects, from internal to DSL modules and so forth. Asynchronous testing generally offers a considerable +performance gain in code. + + +Summary: + +- The dependency you need is ```"com.outworkers" %% "util-testing" % UtilVersion```. +- You have to import ```com.outworkers.util.testing._```. +- You have three main assertion methods, ```successful```, ```failing```, and ```failingWith```. +- You can configure the timeout of waiters with ```implicit val timeout: PatienceConfiguration.Timeout = timeout(20 seconds)```. +- The default timeout value is ```1 second```. + + +### Data sampling ### +Back to top + +This is a very common pattern we use in our testing and it's very easy to interchange this generation with something like ScalaCheck. The idea is very simple, you use type classes to define ways to sample a given type. +After you define such a one-time sampling type class instance, you have access to several methods that will allow you to generate test data. + +It's useful to define such typeclass instances inside package objects, as they will be "invisibly" imported in to the scope you need them to. This is often really neat, albeit potentially confusing for novice Scala users. + + +```scala + +import com.outworkers.util.testing._ + +case class MyAwesomeClass( + name: String, + age: Int, + email: String +) +``` + +You may notice this pattern is already available in better libraries such as ScalaMock and we are not trying to provide an alternative to ScalaMock or compete with it in any way. Our typeclass generator approach only becomes very useful where you really care about very specific properties of the data. +For instance, you may want to get a user with a valid email address, or you may use the underlying factories to get a name that reassembles the name of a real person, and so on. + +It's also useful when you want to define specific ways in which hierarchies of classes are composed together into a sample. If generation for the sake of generation is all you care about, then ScalaMock is probably more robust. + + +### Automated sampling + +Back to top + +One interesting thing that happens when using the `@sample` annotation is that using `gen` immediately after it will basically +give you an instance of your `case class` with the fields appropriately pre-filled, and some of the basic scenarios are also name aware. + +What this means is that we try to make the data feel "real" with respect to what it should be. Let's take the below example: + +```scala + +import java.util.UUID + +case class User( + id: UUID, + firstName: String, + lastName: String, + email: String +) +``` +This is interesting and common enough. What's more interesting is the output of `gen`. + +```scala +import com.outworkers.util.samplers._ + +object Examplers { + val user = gen[User] + + user.trace() + + /** + User( + id = 6be8914c-4274-40ee-83f5-334131246fd8 + firstName = Lindsey + lastName = Craft + email = rparker@hotma1l.us + ) + */ +} + +``` + +So as you can see, the fields have been appropriately pre-filled. The email is a valid email, and the first and last name look like first and last names. For +anything that's in the default generation domain, including dates and country codes and much more, we have the ability to produce automated +appropriate values. + +During the macro expansion phase, we check the annotation targets and try to infer the "natural" value based on the field name and type. So +if your field name is either "email" or "emailAddress" or anything similar enough, you will get an "email" back. + + +It is also possible to generate deeply nested case classes. + +```scala + +case class Address( + postcode: String, + firstLine: String, + secondLine: String, + thirdLine: String, + city: String, + country: String +) + +case class GeoLocation( + longitude: BigDecimal, + latitude: BigDecimal +) + +case class LocatedUser( + geo: GeoLocation, + address: Address, + user: User +) + +object GenerationExamples { + val deeplyNested = gen[LocatedUser] +} + +``` + +#### Creating custom samplers and adding new types + +The automated sampling capability is a fairly simple but useful party trick. It relies +on the framework knowing how to generate basic things, such as `Int`, `Boolean`, `String`, +and so on, and the framework can then compose from these samplers to build them up +into any hierarchy of case classes you need. + +But sometimes it will come short when it doesn't know how to generate a specific type. For example, +let's look at how we could deal with `java.sql.Date`, which has no implicit sample available by default. + +```scala + +case class ExpansionExample( + id: UUID, + date: java.sql.Date +) +``` + +Let's try to write some tests around the sampler. All we need to do is create a sampler for `java.sql.Date`. + +```scala + +import org.scalatest.{ FlatSpec, Matchers } +import java.time.{LocalDate, ZoneId} + +class MyAwesomeSpec extends FlatSpec with Matchers { + + implicit val sqlDateSampler = new Sample[java.sql.Date] { + override def sample: java.sql.Date = java.sql.Date.valueOf(LocalDate.now(ZoneId.of("UTC"))) + } + + "The samplers lib" should "automatically sample an instance of ExpansionExample" in { + val instance = gen[ExpansionExample] + } +} + +``` + +Now, no matter how deeply nested in a case class structure the `java.sql.Date` is located inside a case class, +the framework is capable of finding it as long as it's available in the implicit scope where the `gen` method is called. + + + +#### Working with options. + + + +### Generating data + +There are multiple methods available, allowing you to generate more than just the type: + +- ```gen[T]```, used to generate a single instance of T. +- ```gen[X, Y]```, used to generate a tuple based on two samples. +- ```genOpt[T]```, convenience method that will give you back a ```Some[T](..)```. +- ```genList[T](limit)```, convenience method that will give you back a ```List[T]```. The numbers of items in the list is equal to the ```limit``` and has a default value of 5 if not specified. +- ```genMap[T]()```, convenience method that will give you back a ```Map[String, T]```. + + +There is also a default list of available generators for some default types, and to get to their value simply use the `value` method if the type is not a primitive. For things like ```EmailAddress```, the point of the extra class is obviously to distinguish the type during implicit resolution, but you don't need to use our abstraction at all, there will always be an easy way to get to the underlying generated primitives. + +In the case of email addresses, you can use ```gen[EmailAddress].value```, which will correctly generate a valid ```EmailAddress``` but you can work directly with a ```String```. + +- ```scala.Int``` +- ```scala.Double``` +- ```scala.Float``` +- ```scala.Long``` +- ```scala.String``` +- ```scala.math.BigDecimal``` +- ```scala.math.BigInt``` +- ```java.util.Date``` +- ```java.util.UUID``` +- ```org.joda.time.DateTime``` +- ```org.joda.time.LocalDate``` +- ```com.outworkers.util.domain.Definitions.EmailAddress(value)``` +- ```com.outworkers.util.domain.Definitions.FirstName(value)``` +- ```com.outworkers.util.domain.Definitions.LastName(value)``` +- ```com.outworkers.util.domain.Definitions.FullName(value)``` +- ```com.outworkers.util.domain.Definitions.CountryCode(value)``` +- ```com.outworkers.util.domain.Definitions.Country(value)``` +- ```com.outworkers.util.domain.Definitions.City(value)``` +- ```com.outworkers.util.domain.Definitions.ProgrammingLanguage(value)``` +- ```com.outworkers.util.domain.Definitions.LoremIpsum(value)``` + + +### util-parsers ### +Back to top + +The parser module features an easy to use and integrate set of ScalaZ Applicative based parsers, with an ```Option``` based parser variant. It allows us to +seamlessly deal with validation chains at REST API level or whenever validation is involved. Whether it's monadic composition of options or chaining of +applicative functors to obtain a "correct" chain, the parser module is designed to offer an all-you-can-eat buffet of mini parsers that can be easily +composed to suit any validation needs. + +Each parser comes in three distinct flavours, a ```ValidationNel``` parser that parsers the end type from a ```String``` and returns the type itself, +a parser that parses an end result from an ```Option[String]``` and parserOpt variant that returns an ```Option[T]``` instead of a ```ValidationNel[String, +T]```, which allows for Monadic composition, where you need to "short-circuit" evaluation and validation, instead of computing the full chain by chaining +applicatives. + +### Option parsers ### +Back to top + +The full list of optional parsers is: + +| Type | Input type | Parser Output type | +| --------------- |---------------------------| --------------------------------- | +| Int | String\|Option[String] | ValidationNel[String, Int] | +| Long | String\|Option[String] | ValidationNel[String, Long] | +| Double | String\|Option[String] | ValidationNel[String, Double] | +| Float | String\|Option[String] | ValidationNel[String, Float] | +| UUID | String\|Option[String] | ValidationNel[String, UUID] | +| Email | String\|Option[String] | ValidationNel[String, String] | +| DateTime | String\|Option[String] | ValidationNel[String, org.joda.time.DateTime] | + +Option parsers are designed for chains where you want to short-circuit and exit to result as soon a parser fails. This short-circuit behaviour is the default + ```flatMap``` behaviour of an ```Option```, as soon as an ```Option``` is ```None``` the chain breaks. Unlike applicatives, + the evaluation sequence of options will be escaped and you cannot for instance return an error for every parser that couldn't validate. Instead, + you will only get the first error in the sequence. + +An example of how to use ```Option``` parsers might be: + +```scala + +import com.outworkers.util.parsers._ + +object Test { + def optionalParsing(email: String, age: String): Option[String] = { + for { + validEmail <- parseOpt[EmailAddress](email) + validAge <- parseOpt[Int](age) + } yield s"This person can be reached at $validEmail and is $validAge years old" + } +} + +``` + + +### Applicative parsers ### +Back to top + +The full list of ScalaZ Validation based applicative parsers is: + +| Type | Input type | Parser Output type | +| --------------- |---------------------------| --------------------------------- | +| Int | String\|Option[String] | ValidationNel[String, Int] | +| Long | String\|Option[String] | ValidationNel[String, Long] | +| Double | String\|Option[String] | ValidationNel[String, Double] | +| Float | String\|Option[String] | ValidationNel[String, Float] | +| UUID | String\|Option[String] | ValidationNel[String, UUID] | +| Email | String\|Option[String] | ValidationNel[String, String] | +| DateTime | String\|Option[String] | ValidationNel[String, org.joda.time.DateTime] | + +To illustrate the basic usage of applicative parsers and how to chain them, have a look below. + +```scala + +import scalaz._ +import scalaz.Scalaz._ +import com.outworkers.util.parsers._ + +case class UserToRegister( + email: String, + age: Int +) + +object Test { + + def registerUser(str: String, age: String): Validation[NonEmptyList[String], UserToRegister] = { + (parse[EmailAddress](str) |@| parse[Int](age)) { + (validEmail, validAge) => UserToRegister(validEmail.value, validAge) + } + } + +} +``` + +### Contributors +Back to top + +- Flavian Alexandru @alexflav23 +- Jens Halm @jenshalm +- Bartosz Jankiewicz @bjankie1 + + +Copyright +=============================== +Back to top + +Copyright (c) 2014 - 2016 outworkers. diff --git a/project/plugins.sbt b/project/plugins.sbt index 381d999..e1568fc 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -22,6 +22,7 @@ addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.3.0") addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") +addSbtPlugin("org.tpolecat" % "tut-plugin" % "0.5.6") addSbtPlugin("me.lessis" % "bintray-sbt" % "0.3.0") diff --git a/readme/src/main/tut/README.md b/readme/src/main/tut/README.md index c6c03b4..e9f4e7c 100644 --- a/readme/src/main/tut/README.md +++ b/readme/src/main/tut/README.md @@ -86,63 +86,6 @@ heavily throughout the Outworkers ecosystem of projects, from internal to DSL mo performance gain in code. -### Async assertions ### -Back to top - - -The async assertions module features a dual API, so you can call the same methods on both ```scala.concurrent.Future``` and ```com.twitter.util.Future```. -The underlying mechanism will create an async ```Waiter```, that will wait for the future to complete within the given ```PatienceConfiguration```. The -awaiting is done asynchronously and the assertions are invoked and evaluated once the future in question has returned a result. - -```tut:silent -import com.outworkers.util.testing._ - -class MyTests extends FlatSuite with Matchers { - - "The async computation" should "return 0 on completion" in { - val f: Future[Int] = .. // Pretend this is a Future just like any other future. - f.successful { - res => { - res shouldEqual 0 - } - } - } - - "This async computation" should "fail by design" in { - val f: Future[Unit] = .. - - // You don't even need to do anything more than failure at this stage. - // If the Future fails, the test will succeed, as this method is used when you "expect a failure". - // You can however perform assertions on the error returned. - f.failing { - err => { - } - } - } - - "This async computation" should "fail with a specific error" in { - val f: Future[Unit] = .. - f.failingWith[NumberFormatException] { - err => { - } - } - } - -} -``` - - -You can directly customise the ```timeout``` of all ```Waiters``` using the ScalaTest specific time span implementations and interval configurations. - - -```scala -import org.scalatest.concurrent.PatienceConfiguration -import org.scalatest.time.SpanSugar._ - -implicit val timeout: PatienceConfiguration.Timeout = timeout(20 seconds) - -``` - Summary: - The dependency you need is ```"com.outworkers" %% "util-testing" % UtilVersion```. @@ -161,11 +104,15 @@ After you define such a one-time sampling type class instance, you have access t It's useful to define such typeclass instances inside package objects, as they will be "invisibly" imported in to the scope you need them to. This is often really neat, albeit potentially confusing for novice Scala users. -```tut:passthrough +```tut:silent import com.outworkers.util.testing._ -@sample case class MyAwesomeClass(name: String, age: Int, email: String) +case class MyAwesomeClass( + name: String, + age: Int, + email: String +) ``` You may notice this pattern is already available in better libraries such as ScalaMock and we are not trying to provide an alternative to ScalaMock or compete with it in any way. Our typeclass generator approach only becomes very useful where you really care about very specific properties of the data. @@ -179,13 +126,15 @@ It's also useful when you want to define specific ways in which hierarchies of c Back to top One interesting thing that happens when using the `@sample` annotation is that using `gen` immediately after it will basically -give you an instance of your `case class` with the fields appropiately pre-filled, and some of the basic scenarios are also name aware. +give you an instance of your `case class` with the fields appropriately pre-filled, and some of the basic scenarios are also name aware. What this means is that we try to make the data feel "real" with respect to what it should be. Let's take the below example: -```tut:passthrough +```tut:silent + +import java.util.UUID -@sample case class User( +case class User( id: UUID, firstName: String, lastName: String, @@ -194,20 +143,23 @@ What this means is that we try to make the data feel "real" with respect to what ``` This is interesting and common enough. What's more interesting is the output of `gen`. -```tut:passthrough - -val user = gen[User] - -user.trace() +```tut:silent +import com.outworkers.util.samplers._ -/** -User( - id = 6be8914c-4274-40ee-83f5-334131246fd8 - firstName = Lindsey - lastName = Craft - email = rparker@hotma1l.us -) -*/ +object Examplers { + val user = gen[User] + + user.trace() + + /** + User( + id = 6be8914c-4274-40ee-83f5-334131246fd8 + firstName = Lindsey + lastName = Craft + email = rparker@hotma1l.us + ) + */ +} ``` @@ -219,6 +171,83 @@ During the macro expansion phase, we check the annotation targets and try to inf if your field name is either "email" or "emailAddress" or anything similar enough, you will get an "email" back. +It is also possible to generate deeply nested case classes. + +```tut:silent + +case class Address( + postcode: String, + firstLine: String, + secondLine: String, + thirdLine: String, + city: String, + country: String +) + +case class GeoLocation( + longitude: BigDecimal, + latitude: BigDecimal +) + +case class LocatedUser( + geo: GeoLocation, + address: Address, + user: User +) + +object GenerationExamples { + val deeplyNested = gen[LocatedUser] +} + +``` + +#### Creating custom samplers and adding new types + +The automated sampling capability is a fairly simple but useful party trick. It relies +on the framework knowing how to generate basic things, such as `Int`, `Boolean`, `String`, +and so on, and the framework can then compose from these samplers to build them up +into any hierarchy of case classes you need. + +But sometimes it will come short when it doesn't know how to generate a specific type. For example, +let's look at how we could deal with `java.sql.Date`, which has no implicit sample available by default. + +```tut:silent + +case class ExpansionExample( + id: UUID, + date: java.sql.Date +) +``` + +Let's try to write some tests around the sampler. All we need to do is create a sampler for `java.sql.Date`. + +```tut:silent + +import org.scalatest.{ FlatSpec, Matchers } +import java.time.{LocalDate, ZoneId} + +class MyAwesomeSpec extends FlatSpec with Matchers { + + implicit val sqlDateSampler = new Sample[java.sql.Date] { + override def sample: java.sql.Date = java.sql.Date.valueOf(LocalDate.now(ZoneId.of("UTC"))) + } + + "The samplers lib" should "automatically sample an instance of ExpansionExample" in { + val instance = gen[ExpansionExample] + } +} + +``` + +Now, no matter how deeply nested in a case class structure the `java.sql.Date` is located inside a case class, +the framework is capable of finding it as long as it's available in the implicit scope where the `gen` method is called. + + + +#### Working with options. + + + ### Generating data There are multiple methods available, allowing you to generate more than just the type: @@ -330,14 +359,16 @@ import scalaz._ import scalaz.Scalaz._ import com.outworkers.util.parsers._ +case class UserToRegister( + email: String, + age: Int +) + object Test { - def registerUser(str: String, age: String): Unit = { + def registerUser(str: String, age: String): Validation[NonEmptyList[String], UserToRegister] = { (parse[EmailAddress](str) |@| parse[Int](age)) { - (validEmail, validAge) => { - } - }.fold { - // .. + (validEmail, validAge) => UserToRegister(validEmail.value, validAge) } } diff --git a/util-play/src/test/scala/com/outworkers/util/play/PlayAugmenterTests.scala b/util-play/src/test/scala/com/outworkers/util/play/PlayAugmenterTests.scala index 097be10..d151afb 100644 --- a/util-play/src/test/scala/com/outworkers/util/play/PlayAugmenterTests.scala +++ b/util-play/src/test/scala/com/outworkers/util/play/PlayAugmenterTests.scala @@ -51,7 +51,7 @@ class PlayAugmenterTests extends FlatSpec implicit val defaultTimeout: PatienceConfiguration.Timeout = timeout(defaultTimeoutSpan) - override implicit val patienceConfig = PatienceConfig( + override implicit val patienceConfig: PatienceConfig = PatienceConfig( timeout = defaultTimeoutSpan, interval = Span(defaultScalaInterval, Millis) ) diff --git a/util-samplers/src/main/scala/com/outworkers/util/empty/Empty.scala b/util-samplers/src/main/scala/com/outworkers/util/empty/Empty.scala new file mode 100644 index 0000000..0e75be3 --- /dev/null +++ b/util-samplers/src/main/scala/com/outworkers/util/empty/Empty.scala @@ -0,0 +1,82 @@ +package com.outworkers.util.empty + +import java.net.InetAddress +import java.util.{Date, UUID} + +import com.outworkers.util.domain._ +import com.outworkers.util.samplers.Sample +import org.scalacheck.{Arbitrary, Gen} + +import scala.collection.generic.CanBuildFrom +import scala.util.Random + +trait Empty[T] extends Sample[T] + +object Empty extends EmptyGenerators { + + def apply[T](fn: =>T): Empty[T] = new Empty[T] { + override def sample: T = fn + } + + implicit val emptyString: Empty[String] = apply("") + implicit val emptyInt: Empty[Int] = apply(0) + implicit val emptyShort: Empty[Short] = apply(0.toShort) + implicit val emptyDouble: Empty[Double] = apply(0.toDouble) + implicit val emptyFloat: Empty[Float] = apply(0.toFloat) + implicit val emptyLong: Empty[Long] = apply(0.toLong) + implicit val emptyByte: Empty[Byte] = apply(0.toByte) + implicit val emptyBoolean: Empty[Boolean] = apply(Random.nextBoolean()) + implicit val emptyBigDec: Empty[BigDecimal] = apply(BigDecimal(0)) + implicit val emptyBigInt: Empty[BigInt] = apply(BigInt(0)) + implicit val emptyDate: Empty[Date] = apply(new Date()) + implicit val emptyUUID: Empty[UUID] = apply(UUID.randomUUID()) + implicit val emptyEmailAddress: Empty[EmailAddress] = apply(EmailAddress("")) + implicit val emptyFirstName: Empty[FirstName] = apply(FirstName("")) + implicit val emptyLastName: Empty[LastName] = apply(LastName("")) + implicit val emptyFullName: Empty[FullName] = apply(FullName("")) + implicit val emptyCountryCode: Empty[CountryCode] = apply(CountryCode("")) + implicit val emptyCountry: Empty[Country] = apply(Country("")) + implicit val emptyCity: Empty[City] = apply(City("")) + implicit val emptyInet: Empty[InetAddress] = apply(InetAddress.getByAddress(Array(0, 0, 0 ,0))) + implicit val emptyProgrammingLang: Empty[ProgrammingLanguage] = apply(ProgrammingLanguage("")) + implicit val emptyLoremIpsum: Empty[LoremIpsum] = apply(LoremIpsum("")) + implicit val emptyUrl: Empty[Url] = apply(Url("")) + + /** + * !! Warning !! Black magic going on. This will use the excellent macro compat + * library to macro materialise an instance of the required primitive based on the type argument. + * @tparam T The type parameter to materialise a sample for. + * @return A derived sampler, materialised via implicit blackbox macros. + */ + implicit def materialize[T]: Empty[T] = macro EmptyMacro.materialize[T] + + implicit def arbitrary[T : Empty]: Arbitrary[T] = Arbitrary(generator[T]) + + def generator[T : Empty]: Gen[T] = Gen.delay(void[T]) + + def collection[M[X] <: TraversableOnce[X], T : Empty]( + implicit cbf: CanBuildFrom[Nothing, T, M[T]] + ): Empty[M[T]] = { + new Empty[M[T]] { + override def sample: M[T] = cbf().result() + } + } + + /** + * Derives samplers and encodings for a non standard type. + * @param fn The function that converts a [[T]] instance to a [[T1]] instance. + * @tparam T1 The type you want to derive a sampler for. + * @tparam T The source type of the sampler, must already have a sampler defined for it. + * @return A new sampler that can interact with the target type. + */ + def iso[T : Empty, T1](fn: T => T1): Empty[T1] = Empty(fn(void[T])) + + /** + * Convenience method to materialise the context bound and return a reference to it. + * This is somewhat shorter syntax than using implicitly. + * @tparam RR The type of the sample to retrieve. + * @return A reference to a concrete materialised implementation of a sample for the given type. + */ + def apply[RR]()(implicit ev: Empty[RR]): Empty[RR] = ev + +} diff --git a/util-samplers/src/main/scala/com/outworkers/util/empty/EmptyGenerators.scala b/util-samplers/src/main/scala/com/outworkers/util/empty/EmptyGenerators.scala new file mode 100644 index 0000000..fbdb531 --- /dev/null +++ b/util-samplers/src/main/scala/com/outworkers/util/empty/EmptyGenerators.scala @@ -0,0 +1,38 @@ +package com.outworkers.util.empty + +import org.scalacheck.Gen + +import scala.collection.generic.CanBuildFrom + +trait EmptyGenerators { + + val defaultGeneration = 5 + + /** + * Uses the type class available in implicit scope to mock a certain custom object. + * @tparam T The parameter to mock. + * @return A sample of the given type generated using the implicit sampler. + */ + def void[T : Empty]: T = implicitly[Empty[T]].sample + + def voidOpt[T : Empty]: Option[T] = Option.empty[T] + def voidMap[K : Empty, V: Empty]: Map[K, V] = Map.empty[K, V] + + /** + * Generates a list of elements based on an input collection type. + * @param cbf The implicit builder + * @tparam M The type of collection to build + * @tparam T The type of the underlying sampled type. + * @return A Collection of "size" elements with type T. + */ + def void[M[X] <: TraversableOnce[X], T](size: Int = 0)( + implicit cbf: CanBuildFrom[Nothing, T, M[T]], + proof: Empty[T] + ): M[T] = cbf().result() + + def oneOf[T](list: Seq[T]): T = Gen.oneOf(list).sample.get + + def oneOf[T <: Enumeration](enum: T): T#Value = oneOf(enum.values.toList) +} + +object EmptyGenerators extends EmptyGenerators diff --git a/util-samplers/src/main/scala/com/outworkers/util/empty/EmptyMacro.scala b/util-samplers/src/main/scala/com/outworkers/util/empty/EmptyMacro.scala new file mode 100644 index 0000000..347a48c --- /dev/null +++ b/util-samplers/src/main/scala/com/outworkers/util/empty/EmptyMacro.scala @@ -0,0 +1,274 @@ +package com.outworkers.util.empty + + +import java.net.InetAddress +import java.nio.ByteBuffer +import java.util.{Date, UUID} + +import _root_.com.outworkers.util.macros.{AnnotationToolkit, BlackboxToolbelt} +import com.outworkers.util.samplers._ + +import scala.reflect.macros.blackbox + +@macrocompat.bundle +class EmptyMacro(val c: blackbox.Context) extends AnnotationToolkit with BlackboxToolbelt { + + import c.universe._ + + val prefix = q"com.outworkers.util.empty" + val domainPkg = q"com.outworkers.util.domain" + val definitions = "com.outworkers.util.domain" + + object SamplersSymbols { + val intSymbol: Symbol = typed[Int] + val byteSymbol: Symbol = typed[Byte] + val stringSymbol: Symbol = typed[String] + val boolSymbol: Symbol = typed[Boolean] + val shortSymbol: Symbol = typed[Short] + val longSymbol: Symbol = typed[Long] + val doubleSymbol: Symbol = typed[Double] + val floatSymbol: Symbol = typed[Float] + val dateSymbol: Symbol = typed[Date] + val shortString: Symbol= typed[ShortString] + val listSymbol: Symbol = typed[scala.collection.immutable.List[_]] + val setSymbol: Symbol = typed[scala.collection.immutable.Set[_]] + val mapSymbol: Symbol = typed[scala.collection.immutable.Map[_, _]] + val uuidSymbol: Symbol = typed[UUID] + val inetSymbol: Symbol = typed[InetAddress] + val bigInt: Symbol = typed[BigInt] + val bigDecimal: Symbol = typed[BigDecimal] + val optSymbol: Symbol = typed[Option[_]] + val buffer: Symbol = typed[ByteBuffer] + val enum: Symbol = typed[Enumeration#Value] + val firstName: Symbol = typed[FirstName] + val lastName: Symbol = typed[LastName] + val fullName: Symbol = typed[FullName] + val emailAddress: Symbol = typed[EmailAddress] + val city: Symbol = typed[City] + val country: Symbol = typed[Country] + val countryCode: Symbol = typed[CountryCode] + val programmingLanguage: Symbol = typed[ProgrammingLanguage] + val url: Symbol = typed[Url] + } + + trait TypeExtractor { + + def sources: List[Type] + + def applier: List[Type] => TypeName + + def infer: TypeName = applier(sources) + + def generator: List[Type] => Tree + + def default: Tree = generator(sources) + } + + /** + * Describes a complex collection type. + * The source types are the type arguments that will be needed to produce the collection. + * We have more than one because in the case of a Map for instance we could have multiple ones. + * + * The applier function is a way to produce a new type of the same collection type given an input type. + * It's useful because we often want to derive the types of the generators based on the name of the field, + * to offer the auto-replacement funtionality for collections. + * + * @param sources The source types that are needed to produce the collection type. + * @param applier The applier function that allows producing a collection type from a list of type arguments. + */ + case class MapType( + sources: List[Type], + applier: List[Type] => TypeName, + generator: List[Type] => Tree + ) extends TypeExtractor + + object MapType { + def apply( + source: Type, + applier: List[Type] => TypeName, + generator: List[Type] => Tree + ): Option[MapType] = Some(new MapType(source :: Nil, applier, generator)) + + def unapply(arg: Accessor): Option[MapType] = { + + if (arg.symbol == SamplersSymbols.mapSymbol) { + arg.typeArgs match { + case keyType :: listType :: Nil => Some( + MapType( + List(keyType, listType), + applied => TypeName(s"scala.collection.immutable.Map[..$applied]"), + generator = types => q"$prefix.voidMap[..$types]" + ) + ) + case _ => c.abort(c.enclosingPosition, "Failed to find 2 type arguments for Map type") + } + + + } else { + None + } + } + } + + case class CollectionType( + sources: List[Type], + applier: List[Type] => TypeName, + generator: List[Type] => Tree + ) extends TypeExtractor + + object CollectionType { + def unapply(arg: Accessor): Option[CollectionType] = { + if (arg.symbol == SamplersSymbols.listSymbol) { + arg.typeArgs match { + case sourceTpe :: Nil => Some( + CollectionType( + sources = sourceTpe :: Nil, + applier = applied => TypeName(s"$collectionPkg.List[..$applied]"), + generator = tpe => q"$prefix.Empty.void[$collectionPkg.List, ..$tpe]()" + ) + ) + case _ => c.abort(c.enclosingPosition, "Could not extract inner type argument of List.") + } + } else if (arg.symbol == SamplersSymbols.setSymbol) { + arg.typeArgs match { + case sourceTpe :: Nil => Some( + CollectionType( + sources = sourceTpe :: Nil, + applier = applied => TypeName(s"$collectionPkg.Set[..$applied]"), + generator = tpe => q"$prefix.Empty.void[$collectionPkg.Set, ..$tpe]()" + ) + ) + case _ => c.abort(c.enclosingPosition, "Could not extract inner type argument of Set.") + } + } else { + None + } + } + } + + case class OptionType( + sources: List[Type], + applier: List[Type] => TypeName, + generator: List[Type] => Tree + ) extends TypeExtractor + + object OptionType { + def unapply(arg: Accessor): Option[(OptionType)] = { + + if (arg.symbol == SamplersSymbols.optSymbol) { + arg.typeArgs match { + case head :: Nil => Some( + OptionType( + sources = head :: Nil, + applier = applied => TypeName(s"scala.Option[..$applied]"), + generator = t => q"""$prefix.voidOpt[..$t]""" + ) + ) + case _ => c.abort( + c.enclosingPosition, + s"Expected a single type argument for Option[_], found ${arg.typeArgs.size} instead" + ) + } + } else { + None + } + } + } + + private[this] def deriveSamplerType(accessor: Accessor): Tree = { + accessor match { + case MapType(col) => col.default + case OptionType(opt) => opt.default + case CollectionType(col) => col.default + case _ => q"$prefix.void[${accessor.paramType}]" + } + } + + def caseClassSample( + tpe: Type + ): Tree = { + val applies = caseFields(tpe).map { a => { + q"${a.name} = ${deriveSamplerType(a)}" + } } + + q""" + new $prefix.Empty[$tpe] { + override def sample: $tpe = new $tpe(..$applies) + } + """ + } + + def traversableSample(tpe: Type): Tree = { + val outerSymbol = tpe.typeConstructor + + tpe.typeArgs match { + case inner :: Nil => q""" + new $prefix.Empty[$tpe] { + override def sample: $tpe = $prefix.void[$outerSymbol, $inner]() + } + """ + case _ => c.abort(c.enclosingPosition, "Expected a single type argument for type Collection") + } + } + + def tupleSample(tpe: Type): Tree = { + val comp = tpe.typeSymbol.name.toTermName + + val samplers = tpe.typeArgs.map(t => q"$prefix.void[$t]") + + q""" + new $prefix.Empty[$tpe] { + override def sample: $tpe = $comp.apply(..$samplers) + } + """ + } + + def mapSample(tpe: Type): Tree = { + tpe.typeArgs match { + case k :: v :: Nil => + q""" + new $prefix.Empty[$tpe] { + override def sample: $tpe = _root_.scala.collection.immutable.Map.empty[$k, $v] + } + """ + case _ => c.abort(c.enclosingPosition, "Expected exactly two type arguments to be provided to map") + } + } + + def enumSample(tpe: Type): Tree = { + val comp = c.parse(s"${tpe.toString.replace("#Value", "")}") + + q""" + new $prefix.Empty[$tpe] { + override def sample: $tpe = $prefix.oneOf($comp) + } + """ + } + + def macroImpl(tpe: Type): Tree = { + val symbol = tpe.typeSymbol + + val tree = symbol match { + case SamplersSymbols.mapSymbol => mapSample(tpe) + case sym if tpe <:< typeOf[TraversableOnce[_]] => traversableSample(tpe) + case sym if isTuple(tpe) => tupleSample(tpe) + case SamplersSymbols.enum => enumSample(tpe) + case sym if sym.isClass && sym.asClass.isCaseClass => caseClassSample(tpe) + case _ => c.abort(c.enclosingPosition, s"Cannot derive sampler implementation for $tpe") + } + + if (showTrees) { + echo(showCode(tree)) + } + + if (showCache) { + echo(BlackboxToolbelt.sampleCache.show) + } + + tree + } + + def materialize[T : WeakTypeTag]: Tree = { + memoize[Type, Tree](BlackboxToolbelt.sampleCache)(weakTypeOf[T], macroImpl) + } +} diff --git a/util-samplers/src/main/scala/com/outworkers/util/empty/package.scala b/util-samplers/src/main/scala/com/outworkers/util/empty/package.scala new file mode 100644 index 0000000..734b1a7 --- /dev/null +++ b/util-samplers/src/main/scala/com/outworkers/util/empty/package.scala @@ -0,0 +1,13 @@ +package com.outworkers.util + +import com.outworkers.util.domain.Definitions +import com.outworkers.util.samplers.Tracer + +package object empty extends EmptyGenerators with Definitions { + implicit class Printer[T](val obj: T) extends AnyVal { + def trace()(implicit tracer: Tracer[T]): String = tracer.trace(obj) + } +} + + + diff --git a/util-samplers/src/main/scala/com/outworkers/util/samplers/Generators.scala b/util-samplers/src/main/scala/com/outworkers/util/samplers/Generators.scala index 6c86497..e91c8cd 100644 --- a/util-samplers/src/main/scala/com/outworkers/util/samplers/Generators.scala +++ b/util-samplers/src/main/scala/com/outworkers/util/samplers/Generators.scala @@ -30,6 +30,11 @@ trait Generators { } } + def getConstOpt[T : Sample]: Option[T] = { + Some(gen[T]) + } + + /** * Generates a list of elements based on an input collection type. * @param size The number of elements to generate diff --git a/util-samplers/src/main/scala/com/outworkers/util/samplers/Optins.scala b/util-samplers/src/main/scala/com/outworkers/util/samplers/Optins.scala new file mode 100644 index 0000000..7c6967b --- /dev/null +++ b/util-samplers/src/main/scala/com/outworkers/util/samplers/Optins.scala @@ -0,0 +1,18 @@ +package com.outworkers.util.samplers + +trait FillOptions { + def apply[T : Sample](opt: Option[T]): Option[T] +} + +object Options { + implicit val alwaysFillOptions: FillOptions = new FillOptions { + override def apply[T: Sample](opt: Option[T]): Option[T] = { + opt.orElse(Some(gen[T])) + } + } + implicit val neverFillOptions: FillOptions = new FillOptions { + override def apply[T: Sample](opt: Option[T]): Option[T] = { + Option.empty[T] + } + } +} diff --git a/util-samplers/src/main/scala/com/outworkers/util/samplers/Sample.scala b/util-samplers/src/main/scala/com/outworkers/util/samplers/Sample.scala index b02ec36..a54c4a2 100644 --- a/util-samplers/src/main/scala/com/outworkers/util/samplers/Sample.scala +++ b/util-samplers/src/main/scala/com/outworkers/util/samplers/Sample.scala @@ -184,9 +184,6 @@ object Sample extends Generators { } } - - def iso[T : Sample, T1](fn: T => T1): Sample[T1] = derive(fn) - /** * Derives samplers and encodings for a non standard type. * @param fn The function that converts a [[T]] instance to a [[T1]] instance. @@ -194,6 +191,9 @@ object Sample extends Generators { * @tparam T The source type of the sampler, must already have a sampler defined for it. * @return A new sampler that can interact with the target type. */ + def iso[T : Sample, T1](fn: T => T1): Sample[T1] = derive(fn) + + @deprecated("Use Sample.iso instead", "0.46.0") def derive[T : Sample, T1](fn: T => T1): Sample[T1] = new Sample[T1] { override def sample: T1 = fn(gen[T]) } diff --git a/util-samplers/src/main/scala/com/outworkers/util/samplers/SamplerMacro.scala b/util-samplers/src/main/scala/com/outworkers/util/samplers/SamplerMacro.scala index a96465e..29b02ad 100644 --- a/util-samplers/src/main/scala/com/outworkers/util/samplers/SamplerMacro.scala +++ b/util-samplers/src/main/scala/com/outworkers/util/samplers/SamplerMacro.scala @@ -28,6 +28,7 @@ import scala.reflect.macros.blackbox class SamplerMacro(val c: blackbox.Context) extends AnnotationToolkit with BlackboxToolbelt { import c.universe._ + lazy val fillOptions = typeOf[com.outworkers.util.samplers.FillOptions] val prefix = q"com.outworkers.util.samplers" val domainPkg = q"com.outworkers.util.domain" @@ -196,13 +197,24 @@ class SamplerMacro(val c: blackbox.Context) extends AnnotationToolkit with Black if (arg.symbol == SamplersSymbols.optSymbol) { arg.typeArgs match { - case head :: Nil => Some( - OptionType( - sources = head :: Nil, - applier = applied => TypeName(s"scala.Option[..$applied]"), - generator = t => q"""$prefix.genOpt[..$t]""" + case head :: Nil => { + + val fillOptionsImp = c.inferImplicitValue(fillOptions, silent = true) + + Some( + OptionType( + sources = head :: Nil, + applier = applied => TypeName(s"scala.Option[..$applied]"), + generator = t => { + if (fillOptionsImp.nonEmpty) { + q"""$fillOptionsImp($prefix.genOpt[..$t])""" + } else { + q"""$prefix.genOpt[..$t]""" + } + } + ) ) - ) + } case _ => c.abort( c.enclosingPosition, s"Expected a single type argument for Option[_], found ${arg.typeArgs.size} instead" @@ -219,7 +231,14 @@ class SamplerMacro(val c: blackbox.Context) extends AnnotationToolkit with Black case MapType(col) => col.default case OptionType(opt) => accessor.name match { case KnownField(derived) => { - q"""$prefix.genOpt[$derived].map(_.value)""" + + val fillOptionsImp = c.inferImplicitValue(fillOptions, silent = true) + + if (fillOptionsImp.nonEmpty) { + q"""$fillOptionsImp($prefix.genOpt[$derived]).map(_.value)""" + } else { + q"""$prefix.genOpt[$derived].map(_.value)""" + } } case _ => opt.default } @@ -348,6 +367,6 @@ class SamplerMacro(val c: blackbox.Context) extends AnnotationToolkit with Black } def materialize[T : WeakTypeTag]: Tree = { - memoize[Type, Tree](BlackboxToolbelt.sampleCache)(weakTypeOf[T], macroImpl) + macroImpl(weakTypeOf[T]) } } diff --git a/util-samplers/src/test/scala/com/outworkers/util/empty/EmptyGeneratorsTest.scala b/util-samplers/src/test/scala/com/outworkers/util/empty/EmptyGeneratorsTest.scala new file mode 100644 index 0000000..f86743d --- /dev/null +++ b/util-samplers/src/test/scala/com/outworkers/util/empty/EmptyGeneratorsTest.scala @@ -0,0 +1,83 @@ +/* + * Copyright 2013 - 2017 Outworkers Ltd. + * + * 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 com.outworkers.util.empty + +import org.outworkers.domain.test._ +import org.scalatest.{FlatSpec, Matchers} + +class EmptyGeneratorsTest extends FlatSpec with Matchers { + + it should "generate a sized list based on the given argument" in { + assert(void[List, String]().isEmpty) + } + + it should "generate a sized map based on the given size argument" in { + assert(voidMap[String, String].isEmpty) + } + + it should "generate a sized map of known key and value types" in { + assert(voidMap[String, String].isEmpty) + } + + it should "automatically derive valid samples" in { + val sample = void[User] + } + + it should "automatically derive generator samples for complex case classes" in { + val sample = void[CollectionSample] + val user = void[User] + val tp = void[TupleRecord] + val tpColl = void[TupleCollectionRecord] + } + + it should "automatically derive a generator for a nested case class in a different package" in { + "val sample = void[NestedOtherPackage]" should compile + } + + it should "automatically derive a sample for a nested case class" in { + val sample = void[NestedUser] + sample shouldEqual sample + } + + it should "automatically derive samplers for nested collections" in { + val sample = void[List[List[String]]] + sample shouldEqual sample + } + + + it should "automatically derive dictionaries for nested options" in { + val sample = void[SimpleFoo] + sample shouldEqual sample + } + + it should "automatically sample nested collections" in { + + val sample = void[NestedCollections] + Console.println(sample.trace()) + sample shouldEqual sample + } + + it should "automatically generate a sampler for random collection" in { + val sample = void[IndexedSeq[String]] + sample shouldEqual sample + } + + + it should "automatically generate a sampler for a nested Enumeration inside a CaseClass" in { + val sample = void[IndexedSeq[String]] + sample shouldEqual sample + } +} diff --git a/util-samplers/src/test/scala/com/outworkers/util/samplers/GeneratorsTest.scala b/util-samplers/src/test/scala/com/outworkers/util/samplers/GeneratorsTest.scala index 21ce058..74940b6 100644 --- a/util-samplers/src/test/scala/com/outworkers/util/samplers/GeneratorsTest.scala +++ b/util-samplers/src/test/scala/com/outworkers/util/samplers/GeneratorsTest.scala @@ -17,8 +17,10 @@ package com.outworkers.util.samplers import org.outworkers.domain.test.{NestedCollections, SimpleFoo} import org.scalatest.{FlatSpec, Matchers} +import org.outworkers.domain.test._ +import org.scalatest.prop.GeneratorDrivenPropertyChecks -class GeneratorsTest extends FlatSpec with Matchers { +class GeneratorsTest extends FlatSpec with Matchers with GeneratorDrivenPropertyChecks { it should "generate a sized list based on the given argument" in { val limit = 10 @@ -81,4 +83,17 @@ class GeneratorsTest extends FlatSpec with Matchers { val sample = gen[IndexedSeq[String]] sample shouldEqual sample } + + it should "always generate full options when FillOptions is imported" in { + import com.outworkers.util.samplers.Options.alwaysFillOptions + forAll(Sample.generator[NestedOptions]) { value => + value.collections shouldBe defined + value.firstName shouldBe defined + value.id shouldBe defined + value.name shouldBe defined + value.firstName shouldBe defined + value.user shouldBe defined + } + + } } diff --git a/util-samplers/src/test/scala/com/outworkers/util/samplers/Memebership.scala b/util-samplers/src/test/scala/com/outworkers/util/samplers/Memebership.scala deleted file mode 100644 index d4af514..0000000 --- a/util-samplers/src/test/scala/com/outworkers/util/samplers/Memebership.scala +++ /dev/null @@ -1,19 +0,0 @@ -package com.outworkers.util.samplers - -trait RoleType extends Enumeration { - //represents built-in role types. - type RoleType = Value - - val Leader = Value("leader") - val AllianceMember = Value("member") -} - -object RoleType extends RoleType - -case class Membership( - memberId: String, - entityType: String, - allianceId: String, - role: RoleType.Value = RoleType.Leader, - rankId: String -) \ No newline at end of file diff --git a/util-samplers/src/test/scala/com/outworkers/util/samplers/TracerTests.scala b/util-samplers/src/test/scala/com/outworkers/util/samplers/TracerTests.scala index e824e59..0e0cd21 100644 --- a/util-samplers/src/test/scala/com/outworkers/util/samplers/TracerTests.scala +++ b/util-samplers/src/test/scala/com/outworkers/util/samplers/TracerTests.scala @@ -15,40 +15,36 @@ */ package com.outworkers.util.samplers -import org.scalatest.{ FlatSpec, Matchers } +import java.sql.Date +import java.time.{LocalDate, ZoneId} + +import org.outworkers.domain.test._ +import org.scalatest.{FlatSpec, Matchers} class TracerTests extends FlatSpec with Matchers { it should "automatically derive a tracer for a simple type" in { val sample = gen[User] - sample.trace() "sample.trace()" should compile } it should "automatically derive a tracer for a nested type" in { val sample = gen[NestedUser] - sample.trace() "sample.trace()" should compile } it should "automatically derive a tracer for a nested tuple type" in { val sample = gen[TupleRecord] - sample.trace() "sample.trace()" should compile } it should "automatically derive a tracer for a nested tuple collection type" in { - - implicit val mapTracer = new Tracers.MapLikeTracer[Map, String, String]() - val sample = gen[TupleCollectionRecord] - sample.trace() "sample.trace()" should compile } it should "automatically derive a tracer for a type with collections" in { val sample = gen[CollectionSample] - sample.trace() "sample.trace()" should compile } } diff --git a/util-samplers/src/test/scala/com/outworkers/util/samplers/samples.scala b/util-samplers/src/test/scala/com/outworkers/util/samplers/samples.scala deleted file mode 100644 index 6efd878..0000000 --- a/util-samplers/src/test/scala/com/outworkers/util/samplers/samples.scala +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2013 - 2017 Outworkers Ltd. - * - * 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 com.outworkers.util.samplers - -import java.util.UUID -import org.outworkers.domain.test._ - -case class User( - id: UUID, - firstName: String, - lastName: String, - email: String -) - -case class NestedUser( - timestamp: Long, - user: User -) - -case class CollectionSample( - id: UUID, - firstName: String, - lastName: String, - sh: Short, - b: Byte, - name: String, - email: String, - tests: List[String], - sets: List[String], - map: Map[String, String], - emails: List[String], - opt: Option[Int] -) - -case class TupleRecord(id: UUID, tp: (String, Long)) - -case class TupleCollectionRecord(id: UUID, tuples: List[(Int, String)]) - -case class NestedOtherPackage( - id: UUID, - otherPkg: OtherPackageExample, - emails: List[String] -) diff --git a/util-samplers/src/test/scala/org/outworkers/domain/test/domain.scala b/util-samplers/src/test/scala/org/outworkers/domain/test/domain.scala index 06af257..1019f2c 100644 --- a/util-samplers/src/test/scala/org/outworkers/domain/test/domain.scala +++ b/util-samplers/src/test/scala/org/outworkers/domain/test/domain.scala @@ -37,4 +37,71 @@ case class NestedCollections( nestedListSet: List[Set[String]], props: Map[String, List[String]], doubleProps: Map[Set[String], List[String]] +) + +case class NestedOptions( + id: Option[UUID], + name: Option[String], + firstName: Option[String], + user: Option[User], + collections: Option[CollectionSample] +) + +import java.util.UUID + +import org.outworkers.domain.test._ + +case class User( + id: UUID, + firstName: String, + lastName: String, + email: String +) + +case class NestedUser( + timestamp: Long, + user: User +) + +case class CollectionSample( + id: UUID, + firstName: String, + lastName: String, + sh: Short, + b: Byte, + name: String, + email: String, + tests: List[String], + sets: List[String], + map: Map[String, String], + emails: List[String], + opt: Option[Int] +) + +case class TupleRecord(id: UUID, tp: (String, Long)) + +case class TupleCollectionRecord(id: UUID, tuples: List[(Int, String)]) + +case class NestedOtherPackage( + id: UUID, + otherPkg: OtherPackageExample, + emails: List[String] +) + +trait RoleType extends Enumeration { + //represents built-in role types. + type RoleType = Value + + val Leader = Value("leader") + val AllianceMember = Value("member") +} + +object RoleType extends RoleType + +case class Membership( + memberId: String, + entityType: String, + allianceId: String, + role: RoleType.Value = RoleType.Leader, + rankId: String ) \ No newline at end of file diff --git a/util-testing-twitter/src/main/scala/com/outworkers/util/testing/twitter/package.scala b/util-testing-twitter/src/main/scala/com/outworkers/util/testing/twitter/package.scala index 6d7aa4f..94a243f 100644 --- a/util-testing-twitter/src/main/scala/com/outworkers/util/testing/twitter/package.scala +++ b/util-testing-twitter/src/main/scala/com/outworkers/util/testing/twitter/package.scala @@ -66,7 +66,10 @@ package object twitter { * @param timeout The timeout of the asynchronous Waiter. * @tparam T The error returned by the failing computation. Used to assert error messages. */ - def failing[T <: Throwable]()(implicit mf: Manifest[T], timeout: PatienceConfiguration.Timeout): Unit = { + def failing[T <: Throwable]()( + implicit mf: Manifest[T], + timeout: PatienceConfiguration.Timeout + ): Unit = { val w = new Waiter f onSuccess { _ => w.dismiss()} diff --git a/util-testing/src/main/scala/com/outworkers/util/testing/package.scala b/util-testing/src/main/scala/com/outworkers/util/testing/package.scala index 797e301..78457fe 100644 --- a/util-testing/src/main/scala/com/outworkers/util/testing/package.scala +++ b/util-testing/src/main/scala/com/outworkers/util/testing/package.scala @@ -20,7 +20,7 @@ import com.outworkers.util.samplers.Generators import com.outworkers.util.tags.DefaultTaggedTypes import org.joda.time.{DateTime, DateTimeZone, LocalDate} import org.scalacheck.Gen -import org.scalatest.Assertions +import org.scalatest.{Assertion, Assertions} import org.scalatest.concurrent.{PatienceConfiguration, ScalaFutures, Waiters} import org.scalatest.exceptions.TestFailedException @@ -74,63 +74,4 @@ package object testing extends ScalaFutures ScalaAwait.result(future, duration) } } - - /** - * Augmentation to allow asynchronous assertions of a @code {scala.concurrent.Future}. - * @param f The future to augment. - * @tparam A The underlying type of the computation. - */ - implicit class ScalaFutureAssertions[A](val f: ScalaFuture[A]) extends Assertions with Waiters { - - /** - * Use this to assert an expected asynchronous failure of a @code {com.twitter.util.Future} - * The computation and waiting are both performed asynchronously. - * @param mf The class Manifest to extract class information from. - * @param timeout The timeout of the asynchronous Waiter. - * @tparam T The error returned by the failing computation. Used to assert error messages. - */ - def failing[T <: Throwable]( - implicit mf: Manifest[T], - timeout: PatienceConfiguration.Timeout, - ec: ExecutionContext - ): Unit = { - val w = new Waiter - - f onComplete { - case Success(_) => w.dismiss() - case Failure(e) => w(throw e); w.dismiss() - } - - intercept[T] { - w.await(timeout, dismissals(1)) - } - } - - def failingWith[T <: Throwable](fs: ScalaFuture[_]*)(implicit mf: Manifest[T], ec: ExecutionContext) { - val w = new Waiter - fs foreach (_ onComplete { - case Failure(er) => - w(intercept[T](er)) - w.dismiss() - case Success(_) => w.dismiss() - }) - w.await() - } - - /** - * Use this to assert a successful future computation of a @code {com.twitter.util.Future} - * @param x The computation inside the future to await. This waiting is asynchronous. - * @param timeout The timeout of the future. - */ - @deprecated("Use ScalaTest AsyncAssertions trait instead", "0.31.0") - def successful(x: A => Unit)(implicit timeout: PatienceConfiguration.Timeout, ec: ExecutionContext) : Unit = { - val w = new Waiter - - f onComplete { - case Success(res) => w{x(res)}; w.dismiss() - case Failure(e) => w(throw e); w.dismiss() - } - w.await(timeout, dismissals(1)) - } - } } diff --git a/version.sbt b/version.sbt index bd86591..3720610 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -version in ThisBuild := "0.46.0-SNAPSHOT" +version in ThisBuild := "0.48.0-SNAPSHOT"