Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Run examples with Scala CLI #523

Merged
merged 10 commits into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,23 @@ jobs:
- name: Check code and docs formatting
run: scalafmt --check

check-snippets:

runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.action != 'labeled' # run for 'opened', 'reopened' and 'synchronize'

steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: extractions/setup-just@v2
- uses: coursier/setup-action@v1.3.0
with:
apps: scala-cli sbt
- name: Run snippets from documentation
working-directory: docs
run: just test-snippets

build:

runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ runner.dialect = Scala213Source3
fileOverride {
"glob:**/src/main/scala-3/**" { runner.dialect = scala3 }
"glob:**/src/test/scala-3/**" { runner.dialect = scala3 }
"glob:**/scripts/**" { runner.dialect = scala3 }
// for we we have to:
// - replace ```scala with ```scala mdoc (MkDocs does NOT support these suffixes)
// - run scalafmt --check docs/docs
Expand Down
17 changes: 15 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,13 @@ val ciCommand = (platform: String, scalaSuffix: String) => {
tasks.mkString(" ; ")
}

val publishLocalForTests = {
for {
module <- Vector("chimneyMacroCommons", "chimney", "chimneyCats", "chimneyProtobufs", "chimneyJavaCollections")
moduleVersion <- Vector(module, module + "3")
} yield moduleVersion + "/publishLocal"
}.mkString(" ; ")

val releaseCommand = (tag: Seq[String]) =>
if (tag.nonEmpty) "publishSigned ; sonatypeBundleRelease" else "publishSigned"

Expand Down Expand Up @@ -328,7 +335,7 @@ lazy val root = project
|
|When working with IntelliJ or Scala Metals, edit "val ideScala = ..." and "val idePlatform = ..." within "val versions" in build.sbt to control which Scala version you're currently working with.
|
|If you need to test library locally in a different project, use publishLocal:
|If you need to test library locally in a different project, use publish-local-for-tests or manually publishLocal:
| - chimney-macro-commons (obligatory)
| - chimney
| - cats/java-collections/protobufs integration (optional)
Expand Down Expand Up @@ -360,7 +367,13 @@ lazy val root = project
.alias("ci-native-2_13"),
sbtwelcome
.UsefulTask(ciCommand("Native", "2_12"), "CI pipeline for Scala 2.12 on Scala Native")
.alias("ci-native-2_12")
.alias("ci-native-2_12"),
sbtwelcome
.UsefulTask(
publishLocalForTests,
"Publishes all Scala 2.13 and Scala 3 JVM artifacts to test snippets in documentation"
)
.alias("publish-local-for-tests")
)
)

Expand Down
4 changes: 4 additions & 0 deletions docs/Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ build:

serve: build
docker run --rm -it -p 8000:8000 -v ${PWD}:/docs --env "CI_LATEST_TAG=$(git describe --tags)" mkdocs-chimney-docs

test-snippets:
cd .. && sbt publish-local-for-tests
cd .. && scala-cli run scripts/test-snippets.scala -- "$PWD/docs" "$(sbt -batch -error 'print chimney/version')" "" -1 -1
88 changes: 60 additions & 28 deletions docs/docs/cookbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ If we do not want to enable the same flag(s) in several places, we can define sh
TransformerConfiguration.default.enableMethodAccessors.enableMacrosLogging

transparent inline given PatcherConfiguration[?] =
PatcherConfiguration.ignoreNoneInPatch.enableMacrosLogging
PatcherConfiguration.default.ignoreNoneInPatch.enableMacrosLogging
```

!!! tip
Expand Down Expand Up @@ -347,6 +347,7 @@ new extension methods: `asValidatedNec`, `asValidatedNel`, `asValidatedChain` an
!!! example

```scala
//> using dep org.typelevel::cats-core::2.10.0
//> using dep io.scalaland::chimney-cats::{{ chimney_version() }}

case class RegistrationForm(
Expand Down Expand Up @@ -424,6 +425,7 @@ explanation:
!!! example

```scala
//> using dep org.typelevel::cats-core::2.10.0
//> using dep io.scalaland::chimney-cats::{{ chimney_version() }}
import cats.syntax.all._
import io.scalaland.chimney.Transformer
Expand All @@ -442,6 +444,7 @@ Similarly, there exists instances for `PartialTransformer` and `partial.Result`:
!!! example

```scala
//> using dep org.typelevel::cats-core::2.10.0
//> using dep io.scalaland::chimney-cats::{{ chimney_version() }}
import cats.syntax.all._
import io.scalaland.chimney.PartialTransformer
Expand Down Expand Up @@ -520,6 +523,7 @@ What does it means for us?
but to NOT disable parallel semantics for some transformations when we would pass `failFast = false` later on

```scala
//> using dep org.typelevel::cats-core::2.10.0
//> using dep io.scalaland::chimney-cats::{{ chimney_version() }}
import cats.syntax.all._
import io.scalaland.chimney.PartialTransformer
Expand All @@ -541,6 +545,7 @@ What does it means for us?
And `partial.Result`s have to use explicit combinators to decide whether it's sequential or parallel semantics:

```scala
//> using dep org.typelevel::cats-core::2.10.0
//> using dep io.scalaland::chimney-cats::{{ chimney_version() }}
import cats.syntax.all._
import io.scalaland.chimney.partial
Expand Down Expand Up @@ -593,23 +598,26 @@ The automatic conversion into a protobuf with such a field can be problematic:
//> using dep io.scalaland::chimney::{{ chimney_version() }}
import io.scalaland.chimney.dsl._

object scalapb {
case class UnknownFieldSet()
}

object domain {
case class Address(line1: String, line2: String)
}
object protobuf {
case class Address(
line1: String = "",
line2: String = "",
unknownFields: UnknownFieldSet = UnknownFieldSet()
unknownFields: scalapb.UnknownFieldSet = scalapb.UnknownFieldSet()
)
}

domain.Address("a", "b").transformInto[protobuf.Address]
// error: Chimney can't derive transformation from domain.Address to protobuf.Address
//
// protobuf.Address
// unknownFields: UnknownFieldSet - no accessor named unknownFields in source type domain.Address
//
// unknownFields: scalapb.UnknownFieldSet - no accessor named unknownFields in source type domain.Address
//
// Consult https://scalalandio.github.io/chimney for usage examples.
```
Expand All @@ -621,20 +629,18 @@ There are 2 ways in which Chimney could handle this issue:
!!! example

```scala
//> using dep io.scalaland::chimney::{{ chimney_version() }}
import io.scalaland.chimney.dsl._

domain.Address("a", "b").into[protobuf.Address].enableDefaultValues.transform
domain
.Address("a", "b")
.into[protobuf.Address]
.enableDefaultValues
.transform
```

- manually [setting this one field](supported-transformations.md#wiring-constructors-parameter-to-raw-value)_

!!! example

```scala
//> using dep io.scalaland::chimney::{{ chimney_version() }}
import io.scalaland.chimney.dsl._

domain
.Address("a", "b")
.into[protobuf.Address]
Expand Down Expand Up @@ -1007,11 +1013,12 @@ If there is no common interface that could be summoned as implicit for performin
!!! example

Assuming Scala 3 or `-Xsource:3` for fixed `private` constructors so that `Username.apply` and `.copy` would
be private.
be private. (Newest versions of Scala 2.13 additionally require us to acknowledge this change in the behavior by
manually suppressing an error/warning).

```scala
//> using scala {{ scala.2_13 }}
//> using options -Xsource:3
//> using options -Xsource:3 -Wconf:cat=scala3-migration:s
final case class Username private (value: String)
object Username {
def parse(value: String): Either[String, Username] =
Expand All @@ -1035,7 +1042,17 @@ then Partial Transformer would have to be created manually:
!!! example

```scala
//> using scala {{ scala.2_13 }}
//> using options -Xsource:3 -Wconf:cat=scala3-migration:s
//> using dep io.scalaland::chimney::{{ chimney_version() }}

final case class Username private (value: String)
object Username {
def parse(value: String): Either[String, Username] =
if (value.isEmpty) Left("Username cannot be empty")
else Right(Username(value))
}

import io.scalaland.chimney.PartialTransformer
import io.scalaland.chimney.partial

Expand Down Expand Up @@ -1068,16 +1085,29 @@ we could use it to construct `PartialTransformer` automatically:
!!! example

```scala
//> using scala {{ scala.2_13 }}
//> using options -Xsource:3 -Wconf:cat=scala3-migration:s
//> using dep io.scalaland::chimney::{{ chimney_version() }}
import io.scalaland.chimney.PartialTransformer
import io.scalaland.chimney.partial

trait SmartConstructor[From, To] {
def parse(from: From): Either[String, To]
}

implicit def smartConstructedPartial[From, To](implicit
smartConstructor: SmartConstructor[From, To]
): PartialTransformer[From, To] =
PartialTransformer[From, To] { value =>
partial.Result.fromEitherString(smartConstructor.parse(value))
}

final case class Username private (value: String)
object Username extends SmartConstructor[String, Username] {
def parse(value: String): Either[String, Username] =
if (value.isEmpty) Left("Username cannot be empty")
else Right(Username(value))
}
```

The same would be true about extracting values from smart-constructed types
Expand Down Expand Up @@ -1164,7 +1194,7 @@ We can use them to provide unwrapping `Transformer` and wrapping
): Transformer[Outer, Inner] = extractor.extract(_)

implicit def wrapNewType[Inner, Outer](implicit
builder: HasBuilder.Aux[Inner, Outer]
builder: HasBuilder.Aux[Outer, Inner]
): PartialTransformer[Inner, Outer] = PartialTransformer[Inner, Outer] { value =>
partial.Result.fromEitherString(
builder.build(value).left.map(_.toReadableString)
Expand All @@ -1180,11 +1210,11 @@ popular constraints as long as we express them in the value's type.
!!! example

```scala
//> using dep eu.timepit::refined::0.11.0
//> using dep eu.timepit::refined::0.11.1
import eu.timepit.refined._
import eu.timepit.refined.api.Refined
import eu.timepit.refined.auto._
import eu.timepit.refined.collections._
import eu.timepit.refined.collection._

type Username = String Refined NonEmpty
```
Expand All @@ -1194,8 +1224,9 @@ We can validate using the dedicated type class (`Validate`), while extraction is
!!! example

```scala
//> using dep eu.timepit::refined::0.11.0
//> using dep eu.timepit::refined::0.11.1
//> using dep io.scalaland::chimney::{{ chimney_version() }}
import eu.timepit.refined.refineV
import eu.timepit.refined.api.{Refined, Validate}
import io.scalaland.chimney.{PartialTransformer, Transformer}
import io.scalaland.chimney.partial
Expand All @@ -1207,9 +1238,7 @@ We can validate using the dedicated type class (`Validate`), while extraction is
validate: Validate.Plain[Type, Refinement]
): PartialTransformer[Type, Type Refined Refinement] =
PartialTransformer[Type, Type Refined Refinement] { value =>
partial.Result.fromOption(
validate.validate(value).fold(Some(_), _ => None)
)
partial.Result.fromEitherString(refineV[Refinement](value))
}
```

Expand Down Expand Up @@ -1307,12 +1336,13 @@ Most of the time a collection doesn't perform any sort of validations, and you c
}
object MyCollection {

def of[A](as: A*): MyCollection[A] = new MyCollection(Vector(as*))
def of[A](as: A*): MyCollection[A] = new MyCollection(as.toVector)
def from[A](vector: Vector[A]): MyCollection[A] = new MyCollection(vector)
}

// ...you can provide Chimney support for it...
import io.scalaland.chimney.integrations.{ FactoryCompat, TotallyBuildIterable }
import scala.collection.compat._
import scala.collection.mutable

implicit def myCollectionIsTotallyBuildIterable[A]: TotallyBuildIterable[MyCollection[A], A] =
Expand Down Expand Up @@ -1340,8 +1370,8 @@ Most of the time a collection doesn't perform any sort of validations, and you c
import io.scalaland.chimney.dsl._

// for converting to and from standard library collection (or any other type supported this way)
MyCollection("a", "b").transformInto[List[String]] // List("a", "b")
List("a", "b").transformInto[MyCollection[String]] // MyCollection("a", "b")
MyCollection.of("a", "b").transformInto[List[String]] // List("a", "b")
List("a", "b").transformInto[MyCollection[String]] // MyCollection.of("a", "b")

case class Foo(value: String)
case class Bar(value: String, another: Double)
Expand All @@ -1350,7 +1380,7 @@ Most of the time a collection doesn't perform any sort of validations, and you c
List(Foo("test"))
.into[MyCollection[Bar]]
.withFieldConst(_.everyItem.another, 3.14)
.transform // MyCollection(Bar("test", 3.14))
.transform // MyCollection.of(Bar("test", 3.14))
```

If your collection performs some sort of validation, you integrate it with Chimney as well:
Expand All @@ -1373,14 +1403,15 @@ If your collection performs some sort of validation, you integrate it with Chimn
}
object NonEmptyCollection {

def of[A](a: A, as: A*): NonEmptyCollection[A] = new NonEmptyCollection(Vector((a +: as)*))
def of[A](a: A, as: A*): NonEmptyCollection[A] = new NonEmptyCollection(a +: as.toVector)
def from[A](vector: Vector[A]): Option[NonEmptyCollection[A]] =
if (vector.nonEmpty) Some(new NonEmptyCollection(vector)) else None
}

// ...you can provide Chimney support for it...
import io.scalaland.chimney.integrations.{ FactoryCompat, PartiallyBuildIterable }
import io.scalaland.chimney.partial
import scala.collection.compat._
import scala.collection.mutable

implicit def nonEmptyCollectionIsPartiallyBuildIterable[A]: PartiallyBuildIterable[NonEmptyCollection[A], A] =
Expand Down Expand Up @@ -1410,7 +1441,7 @@ If your collection performs some sort of validation, you integrate it with Chimn
import io.scalaland.chimney.dsl._

// for validating that your collection can be created once all items have been put into Builder
List("a").transformIntoPartial[NonEmptyCollection[String]].asOption // Some(NonEmptyCollection("a"))
List("a").transformIntoPartial[NonEmptyCollection[String]].asOption // Some(NonEmptyCollection.of("a"))
List.empty[String].transformIntoPartial[NonEmptyCollection[String]].asOption // None
```

Expand All @@ -1423,6 +1454,7 @@ For map types there are specialized versions of these type classes:

import io.scalaland.chimney.integrations._
import io.scalaland.chimney.partial
import scala.collection.compat._
import scala.collection.mutable

class MyMap[+K, +V] private (private val impl: Vector[(K, V)]) {
Expand All @@ -1437,7 +1469,7 @@ For map types there are specialized versions of these type classes:
}
object MyMap {

def of[K, V](pairs: (K, V)*): MyMap[K, V] = new MyMap(Vector(pairs*))
def of[K, V](pairs: (K, V)*): MyMap[K, V] = new MyMap(pairs.toVector)
def from[K, V](vector: Vector[(K, V)]): MyMap[K, V] = new MyMap(vector)
}

Expand Down Expand Up @@ -1473,7 +1505,7 @@ For map types there are specialized versions of these type classes:
}
object NonEmptyMap {

def of[K, V](pair: (K, V), pairs: (K, V)*): NonEmptyMap[K, V] = new NonEmptyMap(Vector((pair +: pairs)*))
def of[K, V](pair: (K, V), pairs: (K, V)*): NonEmptyMap[K, V] = new NonEmptyMap(pair +: pairs.toVector)
def from[K, V](vector: Vector[(K, V)]): Option[NonEmptyMap[K, V]] =
if (vector.nonEmpty) Some(new NonEmptyMap(vector)) else None
}
Expand Down
7 changes: 5 additions & 2 deletions docs/docs/supported-patching.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ When the patch `case class` contains a field that does not exist in patched obje
val user = User(10, "abc@@domain.com", 1234567890L)

user.patchUsing(UserUpdateForm("xyz@@domain.com", 123123123L, "some address"))
// Chimney can't derive patcher for User with patch type UserUpdateForm
// error:
// Chimney can't derive patching for User with patch type UserUpdateForm
//
// Field named 'address' not found in target patching type snippet.User!
//
// Field named 'address' not found in target patching type User!
// Consult https://chimney.readthedocs.io for usage examples.
```

Expand Down Expand Up @@ -109,6 +111,7 @@ If the flag was enabled in the implicit config it can be disabled with `.failRed
.using(UserUpdateForm("xyz@@domain.com", 123123123L, "some address"))
.failRedundantPatcherFields
.patch
// error:
// Chimney can't derive patcher for User with patch type UserUpdateForm
//
// Field named 'address' not found in target patching type User!
Expand Down
Loading
Loading