fs2-data
repository·main·Indexed 41 days ago
https://github.com/gnieh/fs2-dataA collection of streaming data parsers built on the fs2 library for efficient data processing in Scala. It includes tools for CSV parsing via CellDecoder and RowDecoderF, as well as a finite-state machine parser module featuring Pattern and Rhs for structural data matching and rule transitions.
What's inside fs2-data
- fs2-data is a collection of libraries designed for streaming parsing and transformation of data. It is built to be lightweight, aiming for minimal dependencies (primarily relying on fs2). It provides streaming support for various data formats including JSON, XML, CSV, CBOR, and MessagePack.
Implement JSON queries with fs2-data-json
mainThe
fs2-data-jsonmodule provides a streaming, declarative implementation of ajq-like query language. This allows you to extract and transform specific parts of a JSON input stream.Note: The JSON query feature is currently experimental.
import fs2._ import fs2.data.json._ // Example setup: converting a string input into a stream of JSON tokens val input = "{\"field1\": 0, \"field2\": \"test\", \"field3\": [1, 2, 3]}" val stream = Stream.emit(input).through(tokens[SyncIO, String])Build and eventify XML DOMs
mainThe
fs2.data.xml.dompackage allows you to convert between XML event streams and a Document Object Model (DOM).Building a DOM from events
Use the
documentspipe. This requires an implicitDocumentBuilder[Doc]in scope, which defines how your specificDoctype is constructed from events.Converting a DOM back to events
Use the
eventifypipe to transform a stream ofDocobjects back into a stream of XML events. This requires an implicitDocumentEventifier[Doc]in scope.import dom._ trait SomeDocType // Building DOM implicit val builder: DocumentBuilder[SomeDocType] = ??? stream.through(documents[IO, SomeDocType]) // Eventifying DOM implicit val builder: DocumentBuilder[SomeDocType] = ??? implicit val eventifier: DocumentEventifier[SomeDocType] = ??? stream.through(documents[IO, SomeDocType]) .through(eventify[IO, SomeDocType])Understand StaticHeaders in generic derivation
mainWhen you derive a
CsvRowDecoderorCsvRowEncoderusing the generic modules (automatic or semi-automatic), the resulting instances also implement theStaticHeaderstype class.This implementation allows these decoders/encoders to be used with CSV pipes that require header information to be available at compile-time/statically, such as:
encodeUsingFirstHeadersencodeUsingHeaders
Understand the fs2-data-msgpack API namespaces
mainThe
fs2-data-msgpackmodule is divided into two primary namespaces depending on whether you want to work with Scala types or raw MessagePack structures:fs2.data.msgpack.high: The recommended API for most users. It works directly with Scala values. You defineMsgpackSerializer[A]andMsgpackDeserializer[A]to map your classes to MessagePack.fs2.data.msgpack.low: A low-level API that operates on a flat model ofMsgpackItems. This model mimics the raw MessagePack format and serves as the intermediate layer between binary data and the high-level Scala API.
How to read, transform, and write JSON data with fs2-data
mainThe
fs2-dataapproach to processing JSON follows a linear pipeline:- Reading: Use
fs2operators (likefs2-io) to read raw bytes/text. - Parsing: Use the
json.tokenspipe fromfs2-data-jsonto convert the input stream into JSONTokens. - Transforming: Apply logic to the stream of tokens. This can be done using standard
fs2operators or by using thefs2.data.json.jq.Compilerto turn ajqquery into aPipe. - Printing: Use
json.render.prettyPrint()to convert the transformed tokens back into a formatted JSON string. - Writing: Use
fs2operators (likefs2-io) to write the resulting stream to a destination (e.g., stdout or a file).
- Reading: Use
How the high-level and low-level CSV APIs work together
mainThe
fs2-data-csvmodule provides two layers of abstraction for processing CSV data:- High-level API (
fs2.data.csv._): Provides convenientfs2.Pipes designed for common use cases, such as decoding CSV directly intocase classes or encoding data with headers. It is built by composing low-level pipes. - Low-level API (
fs2.data.csv.lowlevel._): Provides granular pipes that perform single steps in a processing pipeline (e.g., parsing raw characters into rows). These are ideal for building complex, custom CSV processing logic.
Both APIs share core data types and type classes located in
fs2.data.csv._.- High-level API (
Extend fs2-data with new XML DOM libraries
mainTo add support for a new Scala XML DOM library to
fs2-data, you must implement the following two traits:DocumentBuilder: Responsible for building a DOM from a stream of events.DocumentEventifier: Responsible for converting a DOM back into a stream of events.
fs2-dataprovides several implementations out of the box for common libraries.How the jq query language works
mainA query consists of two potential phases:
- Filtering Phase: Selects elements from the input stream (similar to JSONPath). If no selector is provided, the input is selected as a whole.
- Construction Phase: Builds the output based on the selected elements. If no constructor is provided, elements are emitted unchanged.
Query Patterns
filter: Applies only selection (e.g.,.field).filter | constructor: Selects a subset and builds a new structure (e.g.,.field | { "new": . }).constructor: Builds a result from the input top-level value (e.g.,[ . ]).
Constructors can contain sub-queries applied to each element selected during the filtering phase.
Use the high-level CBOR representation
mainThe high-level representation in
fs2.data.cbor.highprovides structured data (AST-like) which is useful for transformations that require knowledge of the data's hierarchy.Parsing
- To parse directly from a byte stream, use the
values[F]pipe. - To convert an existing stream of low-level items into high-level values, use the
parseValues[F]pipe.
Serializing
- To serialize high-level values back to binary, use the
toBinarypipe. - To convert high-level values back into low-level items, use the
toItemspipe.
Controlling Streamed Formats
You can control whether arrays or maps are serialized using indefinite length encoding by setting the
indefiniteflag totrueon theCborValue.Note: When using
toItems, theindefiniteflag onCborValue.ArrayorCborValue.Mapdetermines if they are streamed in the serialized form.import fs2.data.cbor.high._ // Parsing from bytes val valueStream = byteStream.through(values[Fallible]) // Parsing from low-level items val valueStreamFromItems = itemStream.through(parseValues[Fallible]) // Serializing to binary valueStream.through(data.cbor.high.toBinary) // Converting values back to low-level items valueStream.through(toItems) // Example: making top-level arrays streamed (indefinite) valueStream.map { case CborValue.Array(elements, _) => CborValue.Array(elements, true) case value => value }.through(toItems)- To parse directly from a byte stream, use the
Use the low-level CBOR representation
mainThe low-level representation in
fs2.data.cbor.lowfollows the RFC structure as a flat stream of items rather than an Abstract Syntax Tree (AST). This is ideal for processing data that is too large to fit in memory (e.g., collections exceedingInt.MaxValueelements) or when a tree structure is unnecessary.Parsing
To parse a CBOR binary stream into a stream of low-level items, use the
items[F]pipe, whereFis your error handling context (e.g.,Fallible).Serializing
To convert a stream of
CborItems back into a binary representation, use thetoBinary[F]pipe. This pipe validates the input stream and will fail if the stream is invalid (e.g., if an array declaration does not match the number of elements provided).If you require higher throughput and are confident the source is valid, or if you prefer to silently generate potentially invalid byte streams, use the
toNonValidatedBinarypipe instead.import fs2._ import fs2.data.cbor.low._ import scodec.bits._ // Parsing val byteStream = Stream.chunk(Chunk.byteVector(hex"8301820203820405")) val itemStream = byteStream.through(items[Fallible]) // Serializing (Validated) Stream.emits(List(CborItem.StartArray(2), CborItem.TextString("an"), CborItem.TextString("array"))) .through(toBinary[Fallible]) // Serializing (Unvalidated/High-throughput) Stream.emits(List(CborItem.StartArray(3), CborItem.TextString("an"), CborItem.TextString("array"))) .through(toNonValidatedBinary)Handle optional data in CSV rows
mainCSV does not have a standard for missing or null values.
fs2-data-csvprovidesasOptionalAt(forRow) andasOptional(forRow[Header]) to handle these cases.By default, these methods return
Noneif a field is an empty string, but will fail if the field is missing (index out of bounds or header missing). You can customize this behavior by providingmissingandisEmptyfunctions.val testRow = CsvRow.fromNelHeaders(NonEmptyList.of("first" -> "value1", "second" -> "null")) // Custom logic: treat missing fields as None and the string "null" as None def get(field: String) = testRow.asOptional[String](field, missing = _ => Right(None), isEmpty = _ == "null") get("first") // Some(value1) get("second") // None get("third") // None