-
Notifications
You must be signed in to change notification settings - Fork 154
Expand file tree
/
Copy pathAllowsCsvExample.scala
More file actions
98 lines (82 loc) · 4.08 KB
/
AllowsCsvExample.scala
File metadata and controls
98 lines (82 loc) · 4.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package comptime
import zio.blocks.schema._
import zio.blocks.schema.comptime.Allows
import Allows.{Primitive, Record, `|`}
import Allows.{Optional => AOptional}
import util.ShowExpr.show
// ---------------------------------------------------------------------------
// CSV serializer example using Allows[A, S] compile-time shape constraints
//
// A CSV row is a flat record: every field must be a primitive scalar or an
// optional primitive (for nullable columns). Nested records, sequences, and
// maps are all rejected at compile time.
// ---------------------------------------------------------------------------
// Compatible: flat record of primitives and optional primitives
case class Employee(name: String, department: String, salary: BigDecimal, active: Boolean)
object Employee { implicit val schema: Schema[Employee] = Schema.derived }
case class SensorReading(sensorId: String, timestamp: Long, value: Double, unit: Option[String])
object SensorReading { implicit val schema: Schema[SensorReading] = Schema.derived }
object CsvSerializer {
type FlatRow = Primitive | AOptional[Primitive]
/** Serialize a sequence of flat records to CSV format. */
def toCsv[A](rows: Seq[A])(implicit schema: Schema[A], ev: Allows[A, Record[FlatRow]]): String = {
val reflect = schema.reflect.asRecord.get
val header = reflect.fields.map(_.name).mkString(",")
val lines = rows.map { row =>
val dv = schema.toDynamicValue(row)
dv match {
case DynamicValue.Record(fields) =>
fields.map { case (_, v) => csvEscape(dvToString(v)) }.mkString(",")
case _ => ""
}
}
(header +: lines).mkString("\n")
}
private def dvToString(dv: DynamicValue): String = dv match {
case DynamicValue.Primitive(PrimitiveValue.String(s)) => s
case DynamicValue.Primitive(PrimitiveValue.Boolean(b)) => b.toString
case DynamicValue.Primitive(PrimitiveValue.Int(n)) => n.toString
case DynamicValue.Primitive(PrimitiveValue.Long(n)) => n.toString
case DynamicValue.Primitive(PrimitiveValue.Double(n)) => n.toString
case DynamicValue.Primitive(PrimitiveValue.Float(n)) => n.toString
case DynamicValue.Primitive(PrimitiveValue.BigDecimal(n)) => n.toString
case DynamicValue.Primitive(v) => v.toString
case DynamicValue.Null => ""
case DynamicValue.Variant(tag, inner) if tag == "Some" => dvToString(inner)
case DynamicValue.Variant(tag, _) if tag == "None" => ""
case DynamicValue.Record(fields) =>
fields.headOption.map { case (_, v) => dvToString(v) }.getOrElse("")
case other => other.toString
}
private def csvEscape(s: String): String =
if (s.contains(",") || s.contains("\"") || s.contains("\n"))
"\"" + s.replace("\"", "\"\"") + "\""
else s
}
// ---------------------------------------------------------------------------
// Demonstration
// ---------------------------------------------------------------------------
object AllowsCsvExample extends App {
// Flat records of primitives — compiles fine
val employees = Seq(
Employee("Alice", "Engineering", BigDecimal("120000.00"), true),
Employee("Bob", "Marketing", BigDecimal("95000.50"), true),
Employee("Carol", "Engineering", BigDecimal("115000.00"), false)
)
// CSV output for a flat record of primitives
show(CsvSerializer.toCsv(employees))
// Flat record with optional fields — also compiles
val readings = Seq(
SensorReading("temp-01", 1709712000L, 23.5, Some("celsius")),
SensorReading("temp-02", 1709712060L, 72.1, None)
)
// Optional fields become empty CSV cells when None
show(CsvSerializer.toCsv(readings))
// The following would NOT compile — uncomment to see the error:
//
// case class Nested(name: String, address: Address)
// object Nested { implicit val schema: Schema[Nested] = Schema.derived }
// CsvSerializer.toCsv(Seq(Nested("Alice", Address("1 Main St", "NY", "10001"))))
// [error] Schema shape violation at Nested.address: found Record(Address),
// required Primitive | Optional[Primitive]
}