Skip to content

Commit 7242316

Browse files
shnapzMichel Davit
andauthored
Respecting required field of inner case classes in Coder macro (#4645)
* Respecting required field of inner case classes in Coder macro * Fix scala 2.12 compilation * fix scalafix+compile errors * Added negative unsupported test scenarios * Failing on all inner classes * Update scio-test/src/test/scala/com/spotify/scio/coders/CoderTest.scala Co-authored-by: Michel Davit <[email protected]> * Addressing the comment Co-authored-by: Michel Davit <[email protected]>
1 parent ea7cb3d commit 7242316

File tree

3 files changed

+144
-8
lines changed

3 files changed

+144
-8
lines changed

scio-core/src/main/scala/com/spotify/scio/coders/DerivedCoder.scala

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,32 @@ object LowPriorityCoderDerivation {
4141
new CaseClassConstructor(caseClass.getClass.getName)
4242
}
4343

44-
private class CaseClassConstructor[T] private (private val className: String)
45-
extends Serializable {
46-
// We can call rawConstruct on an empty CaseClass instance
47-
@transient lazy val ctx: CaseClass[Coder, T] = ClosureCleaner
48-
.instantiateClass(Class.forName(className))
49-
.asInstanceOf[CaseClass[Coder, T]]
44+
private class CaseClassConstructor[T] private (
45+
private val className: String
46+
) extends Serializable {
47+
48+
@transient lazy val ctxClass: Class[_] = Class.forName(className)
49+
50+
@transient lazy val ctx: CaseClass[Coder, T] = {
51+
ClosureCleaner.outerFieldOf(ctxClass) match {
52+
/* The field "$outer" is added by scala compiler to a case class if it is declared inside
53+
another class. And the constructor of that compiled class requires outer field to be not
54+
null.
55+
If "$outer" is present it's an inner class and this scenario is officially not supported
56+
by Scio */
57+
case Some(_) =>
58+
throw new Throwable(
59+
s"Found an $$outer field in $ctxClass. Possibly it is an attempt to use inner case " +
60+
"class in a Scio transformation. Inner case classes are not supported in Scio " +
61+
"auto-derived macros. Move the case class to the package level or define a custom " +
62+
"coder."
63+
)
64+
/* If "$outer" field is absent then T is not an inner class, we create an empty instance
65+
of ctx */
66+
case None =>
67+
ClosureCleaner.instantiateClass(ctxClass).asInstanceOf[CaseClass[Coder, T]]
68+
}
69+
}
5070

5171
def rawConstruct(fieldValues: Seq[Any]): T = ctx.rawConstruct(fieldValues)
5272
}
@@ -88,6 +108,7 @@ trait LowPriorityCoderDerivation {
88108
def join[T: ClassTag](ctx: CaseClass[Coder, T]): Coder[T] = {
89109
val typeName = ctx.typeName.full
90110
val constructor = CaseClassConstructor(ctx)
111+
91112
if (ctx.isValueClass) {
92113
val p = ctx.parameters.head
93114
Coder.xmap(p.typeclass.asInstanceOf[Coder[Any]])(
@@ -99,6 +120,7 @@ trait LowPriorityCoderDerivation {
99120
} else {
100121
Coder.ref(typeName) {
101122
val cs = Array.ofDim[(String, Coder[Any])](ctx.parameters.length)
123+
102124
ctx.parameters.foreach { p =>
103125
cs.update(p.index, p.label -> p.typeclass.asInstanceOf[Coder[Any]])
104126
}

scio-test/src/main/scala/com/spotify/scio/testing/CoderAssertions.scala

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,20 @@ object CoderAssertions {
4141
def assert(value: T)(implicit c: Coder[T], eq: Equality[T]): Assertion
4242
}
4343

44-
def roundtrip[T](opts: PipelineOptions = DefaultPipelineOptions): CoderAssertion[T] =
44+
def roundtripWithCustomAssert[T](
45+
opts: PipelineOptions = DefaultPipelineOptions
46+
)(customAssertEquality: (T, T) => Assertion): CoderAssertion[T] =
47+
new CoderAssertion[T] {
48+
override def assert(value: T)(implicit c: Coder[T], eq: Equality[T]): Assertion = {
49+
val beamCoder = CoderMaterializer.beamWithDefault(c, o = opts)
50+
val result = roundtripWithCoder(beamCoder, value)
51+
customAssertEquality(value, result)
52+
}
53+
}
54+
55+
def roundtrip[T](
56+
opts: PipelineOptions = DefaultPipelineOptions
57+
): CoderAssertion[T] =
4558
new CoderAssertion[T] {
4659
override def assert(value: T)(implicit c: Coder[T], eq: Equality[T]): Assertion = {
4760
val beamCoder = CoderMaterializer.beamWithDefault(c, o = opts)
@@ -110,4 +123,9 @@ object CoderAssertions {
110123

111124
result should ===(value)
112125
}
126+
127+
private def roundtripWithCoder[T](beamCoder: BCoder[T], value: T): T = {
128+
val bytes = CoderUtils.encodeToByteArray(beamCoder, value)
129+
CoderUtils.decodeFromByteArray(beamCoder, bytes)
130+
}
113131
}

scio-test/src/test/scala/com/spotify/scio/coders/CoderTest.scala

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,39 @@ final case class AnyValExample(value: String) extends AnyVal
121121
// Non deterministic
122122
final case class NonDeterministic(a: Double, b: Double)
123123

124+
class ClassWrapper() {
125+
case class InnerCaseClass(str: String)
126+
127+
def runWithImplicit(implicit
128+
c: Coder[InnerCaseClass]
129+
): Unit =
130+
InnerCaseClass("51") coderShould roundtrip()
131+
132+
def run(): Unit =
133+
InnerCaseClass("51") coderShould roundtrip()
134+
}
135+
136+
object TopLevelObject {
137+
case class InnerCaseClass(str: String)
138+
}
139+
124140
final class CoderTest extends AnyFlatSpec with Matchers {
141+
125142
val userId: UserId = UserId(Seq[Byte](1, 2, 3, 4))
126143
val user: User = User(userId, "johndoe", "[email protected]")
127144

145+
/*
146+
* Case class nested inside another class. Do not move outside
147+
* */
148+
case class InnerCaseClass(str: String)
149+
150+
/*
151+
* Object nested inside another class. Do not move outside
152+
* */
153+
object InnerObject {
154+
case class InnerCaseClass(str: String)
155+
}
156+
128157
def materialize[T](coder: Coder[T]): BCoder[T] =
129158
CoderMaterializer.beam(PipelineOptionsFactory.create(), coder)
130159

@@ -135,7 +164,7 @@ final class CoderTest extends AnyFlatSpec with Matchers {
135164
4.5 coderShould roundtrip()
136165
}
137166

138-
it should "support Scala collections" in {
167+
"Coders" should "support Scala collections" in {
139168
import scala.collection.BitSet
140169

141170
val nil: Seq[String] = Nil
@@ -168,6 +197,73 @@ final class CoderTest extends AnyFlatSpec with Matchers {
168197
CoderProperties.structuralValueConsistentWithEquals(bmc, m, m)
169198
}
170199

200+
"Coders" should "not support inner case classes" in {
201+
{
202+
the[Throwable] thrownBy {
203+
InnerObject coderShould roundtrip()
204+
}
205+
}.getMessage should include(
206+
"Found an $outer field in class com.spotify.scio.coders.CoderTest$$"
207+
)
208+
209+
val cw = new ClassWrapper()
210+
try {
211+
cw.runWithImplicit
212+
throw new Throwable("Is expected to throw when passing implicit from outer class")
213+
} catch {
214+
case e: NullPointerException =>
215+
// In this case outer field is called "$cw" and it is hard to wrap it with proper exception
216+
// so we allow it to fail with NullPointerException
217+
e.getMessage should be(null)
218+
}
219+
220+
{
221+
the[Throwable] thrownBy {
222+
cw.InnerCaseClass("49") coderShould roundtrip()
223+
}
224+
}.getMessage should startWith(
225+
"Found an $outer field in class com.spotify.scio.coders.CoderTest$$"
226+
)
227+
228+
{
229+
the[Throwable] thrownBy {
230+
cw.run()
231+
}
232+
}.getMessage should startWith(
233+
"Found an $outer field in class com.spotify.scio.coders.ClassWrapper$$"
234+
)
235+
236+
{
237+
the[Throwable] thrownBy {
238+
InnerCaseClass("42") coderShould roundtrip()
239+
}
240+
}.getMessage should startWith(
241+
"Found an $outer field in class com.spotify.scio.coders.CoderTest$$"
242+
)
243+
244+
case class ClassInsideMethod(str: String)
245+
246+
{
247+
the[Throwable] thrownBy {
248+
ClassInsideMethod("50") coderShould roundtrip()
249+
}
250+
}.getMessage should startWith(
251+
"Found an $outer field in class com.spotify.scio.coders.CoderTest$$"
252+
)
253+
254+
{
255+
the[Throwable] thrownBy {
256+
InnerObject.InnerCaseClass("42") coderShould roundtrip()
257+
}
258+
}.getMessage should startWith(
259+
"Found an $outer field in class com.spotify.scio.coders.CoderTest$$"
260+
)
261+
}
262+
263+
"Coders" should "support inner classes in objects" in {
264+
TopLevelObject.InnerCaseClass("42") coderShould roundtrip()
265+
}
266+
171267
it should "support tuples" in {
172268
import shapeless.syntax.std.tuple._
173269
val t22 = (

0 commit comments

Comments
 (0)