Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/alexarchambault/data-class
case-classes with better binary compatibility story
https://github.com/alexarchambault/data-class
case-class scala
Last synced: 5 days ago
JSON representation
case-classes with better binary compatibility story
- Host: GitHub
- URL: https://github.com/alexarchambault/data-class
- Owner: alexarchambault
- License: apache-2.0
- Created: 2019-09-30T16:41:50.000Z (about 5 years ago)
- Default Branch: main
- Last Pushed: 2024-08-21T12:55:41.000Z (3 months ago)
- Last Synced: 2024-10-11T08:46:24.616Z (26 days ago)
- Topics: case-class, scala
- Language: Scala
- Homepage:
- Size: 131 KB
- Stars: 79
- Watchers: 8
- Forks: 6
- Open Issues: 13
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# data-class
[![Build Status](https://travis-ci.org/alexarchambault/data-class.svg?branch=master)](https://travis-ci.org/alexarchambault/data-class)
[![Maven Central](https://img.shields.io/maven-central/v/io.github.alexarchambault/data-class_2.13.svg)](https://maven-badges.herokuapp.com/maven-central/io.github.alexarchambault/data-class_2.13)*data-class* allows to create classes almost like case-classes, but with no
public `unapply` or `copy` methods, making it easier to add fields to them while
maintaining binary compatiblity.## Usage
### Setup
Add to your `build.sbt`,
```scala
libraryDependencies += "io.github.alexarchambault" %% "data-class" % "0.2.1"
```The latest version is [![Maven Central](https://img.shields.io/maven-central/v/io.github.alexarchambault/data-class_2.13.svg)](https://maven-badges.herokuapp.com/maven-central/io.github.alexarchambault/data-class_2.13).
The macro paradise plugin is needed up to scala 2.12, and the right
compiler option needs to be used from 2.13 onwards:
```scala
lazy val isAtLeastScala213 = Def.setting {
import Ordering.Implicits._
CrossVersion.partialVersion(scalaVersion.value).exists(_ >= (2, 13))
}
libraryDependencies ++= {
if (isAtLeastScala213.value) Nil
else Seq(compilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full))
}
scalacOptions ++= {
if (isAtLeastScala213.value) Seq("-Ymacro-annotations")
else Nil
}
```Lastly, if you know what you are doing, you can manage to have data-class
[be a compile-time only dependency](https://stackoverflow.com/questions/21515325/add-a-compile-time-only-dependency-in-sbt/21516954#21516954).### API
Use a `@data` annotation instead of a `case` modifier, like
```scala
import dataclass.data@data class Foo(n: Int, s: String)
```This annotation adds a number of features, that can also be found in
case classes:
- sensible `equals` / `hashCode` / `toString` implementations,
- `apply` methods in the companion object for easier creation,
- extend the `scala.Product` trait (itself extending `scala.Equal`), and
implement its methods,
- extend the `scala.Serializable` trait.It also adds things that differ from case classes:
- add `final` modifier to the class,
- for each field, add a corresponding `with` method (field `count: Int`
generates a method `withCount(count: Int)` returning a new instance of the
class with `count` updated).Most notably, it does _not_ generate `copy` or `unapply` methods, making
binary compatibility much more tractable upon adding new fields (see below).In the example above, the `@data` macro generates code like the following (modulo macro hygiene):
```scala
final class Foo(val n: Int, val s: String) extends Product with Serializable {def withN(n: Int) = new Foo(n = n, s = s)
def withS(s: String) = new Foo(n = n, s = s)override def toString: String = {
val b = new StringBuilder("Foo(")
b.append(String.valueOf(n))
b.append(", ")
b.append(String.valueOf(s))
b.append(")")
b.toString
}override def canEqual(obj: Any): Boolean = obj != null && obj.isInstanceOf[Foo]
override def equals(obj: Any): Boolean = this.eq(obj.asInstanceOf[AnyRef]) || canEqual(obj) && {
val other = obj.asInstanceOf[Foo]
n == other.n && s == other.s
})override def hashCode: Int = {
var code = 17 + "Foo".##
code = 37 * code + n.##
code = 37 * code + s.##
37 * code
}private def tuple = (this.n, this.s)
override def productArity: Int = 2
override def productElement(n: Int): Any = n match {
case 0 => this.n
case 1 => this.s
case n => throw new IndexOutOfBoundsException(n.toString)
}
}object Foo {
def apply(n: Int, s: String): Foo = new Foo(n, s)
}
```### shapeless
By default, the classes annotated with `@data` now have a shape that
`shapeless.Generic` handles:
```scala
import dataclass.data@data class Foo(n: Int, d: Double)
import shapeless._
Generic[Foo] // works
```Note that with shapeless `2.3.3` and prior versions, `Generic` derivation may fail
if the body of the `@data` class contains `val`s or `lazy val`s, see
[shapeless issue #934](https://github.com/milessabin/shapeless/issues/934).### Adding fields
In order to retain binary compatibility when adding fields, one should:
- annotate the first added field with `dataclass.since`,
- provide default values for the added fields, like
```scala
import dataclass._@data class Foo(n: Int, d: Double, @since s: String = "", b: Boolean = false)
```The `@since` annotation makes the `@data` macro generate `apply` methods
compatible with those without the new fields.The example above generates the following `apply` methods in the companion object of `Foo`:
```scala
object Foo {
def apply(n: Int, d: Double): Foo = new Foo(n, d, "", false)
def apply(n: Int, d: Double, s: String, b: Boolean) = new Foo(n, d, s, b)
}
```The `@since` annotation accepts an optional string argument - a version
can be passed for example - and it can be used multiple times, like
```scala
import dataclass._@data class Foo(
n: Int,
d: Double,
@since("1.1")
s: String = "",
b: Boolean = false,
@since("1.2")
count: Option[Int] = None,
info: Option[String] = None
)
```This generates the following `apply` methods in the companion object of `Foo`:
```scala
object Foo {
def apply(n: Int, d: Double): Foo = new Foo(n, d, "", false, None, None)
def apply(n: Int, d: Double, s: String, b: Boolean) = new Foo(n, d, s, b, None, None)
def apply(n: Int, d: Double, s: String, b: Boolean, count: Option[Int], info: Option[String]) = new Foo(n, d, s, b, count, info)
}
```## Related work
- [contraband](https://github.com/sbt/contraband) relies on code generation from
JSON or a custom schema language to generate classes that can be evolved in a
binary compatible way
- [stalagmite](https://gitlab.com/fommil/attic/tree/master/stalagmite) generates
case classes with custom features via some macros (but doesn't aim at helping
maintaining binary compatibility)