Skip to content

Commit

Permalink
Refactor: use a params case class for ProtoEncoder
Browse files Browse the repository at this point in the history
Instead of messily updating argument lists and creating a ton of overloaded factory methods, let's just pass a case class instance that contains all parameters for the encoder.

This has two benefits. First, it will be much easier to expand the list of parameters for the ProtoEncoder. Second, handling these params will be much easier in functional code, because now we have them grouped in one tidy object.
  • Loading branch information
Ostrzyciel committed Jan 10, 2025
1 parent c1e32fd commit 20e667c
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,10 @@ trait ConverterFactory[
* Create a new [[ProtoEncoder]] which manages a row buffer on its own. Namespace declarations are disabled.
* @param options Jelly serialization options.
* @return encoder
* @deprecated since 2.6.0; use `encoder(ProtoEncoder.Params)` instead
*/
def encoder(options: RdfStreamOptions): TEncoder = encoder(options, enableNamespaceDeclarations = false, None)
final def encoder(options: RdfStreamOptions): TEncoder =
encoder(options, enableNamespaceDeclarations = false, None)

/**
* Create a new [[ProtoEncoder]] which manages a row buffer on its own.
Expand All @@ -142,8 +144,9 @@ trait ConverterFactory[
* If true, this will raise the stream version to 2 (Jelly 1.1.0). Otherwise,
* the stream version will be 1 (Jelly 1.0.0).
* @return encoder
* @deprecated since 2.6.0; use `encoder(ProtoEncoder.Params)` instead
*/
def encoder(options: RdfStreamOptions, enableNamespaceDeclarations: Boolean): TEncoder =
final def encoder(options: RdfStreamOptions, enableNamespaceDeclarations: Boolean): TEncoder =
encoder(options, enableNamespaceDeclarations, None)

/**
Expand All @@ -157,9 +160,18 @@ trait ConverterFactory[
* If provided, the encoder will append the rows to this buffer instead of
* returning them, so methods like `addTripleStatement` will return Seq().
* @return encoder
* @deprecated since 2.6.0; use `encoder(ProtoEncoder.Params)` instead
*/
def encoder(
final def encoder(
options: RdfStreamOptions,
enableNamespaceDeclarations: Boolean,
maybeRowBuffer: Option[mutable.Buffer[RdfStreamRow]]
): TEncoder
): TEncoder = encoder(ProtoEncoder.Params(options, enableNamespaceDeclarations, maybeRowBuffer))

/**
* Create a new [[ProtoEncoder]].
* @param params Parameters for the encoder.
* @return encoder
* @since 2.6.0
*/
def encoder(params: ProtoEncoder.Params): TEncoder
49 changes: 37 additions & 12 deletions core/src/main/scala/eu/ostrzyciel/jelly/core/ProtoEncoder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,54 @@ object ProtoEncoder:
private val defaultGraphStart = RdfStreamRow(RdfGraphStart(RdfDefaultGraph.defaultInstance))
private val emptyRowBuffer: List[RdfStreamRow] = List()

/**
* Parameters passed to the Jelly encoder.
*
* New fields may be added in the future, but always with a default value and in a sequential order.
* However, it is still recommended to use named arguments when creating this object.
*
* @param options options for this stream (required)
* @param enableNamespaceDeclarations whether to allow namespace declarations in the stream.
* If true, this will raise the stream version to 2 (Jelly 1.1.0). Otherwise,
* the stream version will be 1 (Jelly 1.0.0).
* @param maybeRowBuffer optional buffer for storing stream rows that should go into a stream frame.
* If provided, the encoder will append the rows to this buffer instead of
* returning them, so methods like `addTripleStatement` will return Seq().
*/
final case class Params(
options: RdfStreamOptions,
enableNamespaceDeclarations: Boolean = false,
maybeRowBuffer: Option[mutable.Buffer[RdfStreamRow]] = None,
)

/**
* Stateful encoder of a protobuf RDF stream.
*
* This class supports all stream types and options, but usually does not check if the user is conforming to them.
* It will, for example, allow the user to send generalized triples in a stream that should not have them.
* Take care to ensure the correctness of the transmitted data, or use the specialized wrappers from the stream package.
* @param options options for this stream
* @param enableNamespaceDeclarations whether to allow namespace declarations in the stream.
* If true, this will raise the stream version to 2 (Jelly 1.1.0). Otherwise,
* the stream version will be 1 (Jelly 1.0.0).
* @param maybeRowBuffer optional buffer for storing stream rows that should go into a stream frame.
* If provided, the encoder will append the rows to this buffer instead of
* returning them, so methods like `addTripleStatement` will return Seq().
* @param params parameters object for the encoder
*/
abstract class ProtoEncoder[TNode, -TTriple, -TQuad, -TQuoted](
final val options: RdfStreamOptions,
final val enableNamespaceDeclarations: Boolean,
final val maybeRowBuffer: Option[mutable.Buffer[RdfStreamRow]],
):
abstract class ProtoEncoder[TNode, -TTriple, -TQuad, -TQuoted](params: ProtoEncoder.Params):
import ProtoEncoder.*

// *** 1. THE PUBLIC INTERFACE ***
// *******************************
/**
* RdfStreamOptions for this encoder.
*/
final val options: RdfStreamOptions = params.options

/**
* Whether namespace declarations are enabled for this encoder.
*/
final val enableNamespaceDeclarations: Boolean = params.enableNamespaceDeclarations

/**
* Buffer for storing stream rows that should go into a stream frame.
*/
final val maybeRowBuffer: Option[mutable.Buffer[RdfStreamRow]] = params.maybeRowBuffer

/**
* Add an RDF triple statement to the stream.
* @param triple triple to add
Expand Down
45 changes: 23 additions & 22 deletions core/src/test/scala/eu/ostrzyciel/jelly/core/ProtoEncoderSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,23 @@ import scala.collection.mutable.ListBuffer

class ProtoEncoderSpec extends AnyWordSpec, Matchers:
import ProtoTestCases.*
import ProtoEncoder.Params as Pep

// Test body
"a ProtoEncoder" should {
"encode triple statements" in {
val encoder = MockProtoEncoder(
val encoder = MockProtoEncoder(Pep(
JellyOptions.smallGeneralized.withPhysicalType(PhysicalStreamType.TRIPLES)
)
))
val encoded = Triples1.mrl.flatMap(triple => encoder.addTripleStatement(triple).toSeq)
assertEncoded(encoded, Triples1.encoded(encoder.options.withVersion(Constants.protoVersionNoNsDecl)))
}

"encode triple statements with namespace declarations" in {
val encoder = MockProtoEncoder(
val encoder = MockProtoEncoder(Pep(
JellyOptions.smallGeneralized.withPhysicalType(PhysicalStreamType.TRIPLES),
enableNamespaceDeclarations = true,
)
))
val encoded = Triples2NsDecl.mrl.flatMap {
case t: Triple => encoder.addTripleStatement(t).toSeq
case ns: NamespaceDecl => encoder.declareNamespace(ns.name, ns.iri).toSeq
Expand All @@ -36,10 +37,10 @@ class ProtoEncoderSpec extends AnyWordSpec, Matchers:

"encode triple statements with ns decls and an external buffer" in {
val buffer = ListBuffer[RdfStreamRow]()
val encoder = MockProtoEncoder(
val encoder = MockProtoEncoder(Pep(
JellyOptions.smallGeneralized.withPhysicalType(PhysicalStreamType.TRIPLES),
enableNamespaceDeclarations = true, Some(buffer)
)
))
for triple <- Triples2NsDecl.mrl do
val result = triple match
case t: Triple => encoder.addTripleStatement(t)
Expand All @@ -51,19 +52,19 @@ class ProtoEncoderSpec extends AnyWordSpec, Matchers:
}

"encode quad statements" in {
val encoder = MockProtoEncoder(
val encoder = MockProtoEncoder(Pep(
JellyOptions.smallGeneralized.withPhysicalType(PhysicalStreamType.QUADS)
)
))
val encoded = Quads1.mrl.flatMap(quad => encoder.addQuadStatement(quad).toSeq)
assertEncoded(encoded, Quads1.encoded(encoder.options.withVersion(Constants.protoVersionNoNsDecl)))
}

"encode quad statements with an external buffer" in {
val buffer = ListBuffer[RdfStreamRow]()
val encoder = MockProtoEncoder(
val encoder = MockProtoEncoder(Pep(
JellyOptions.smallGeneralized.withPhysicalType(PhysicalStreamType.QUADS),
false, Some(buffer)
)
))
for quad <- Quads1.mrl do
val result = encoder.addQuadStatement(quad)
// external buffer – nothing should be returned directly
Expand All @@ -73,17 +74,17 @@ class ProtoEncoderSpec extends AnyWordSpec, Matchers:
}

"encode quad statements (repeated default graph)" in {
val encoder = MockProtoEncoder(
val encoder = MockProtoEncoder(Pep(
JellyOptions.smallGeneralized.withPhysicalType(PhysicalStreamType.QUADS)
)
))
val encoded = Quads2RepeatDefault.mrl.flatMap(quad => encoder.addQuadStatement(quad).toSeq)
assertEncoded(encoded, Quads2RepeatDefault.encoded(encoder.options.withVersion(Constants.protoVersionNoNsDecl)))
}

"encode graphs" in {
val encoder = MockProtoEncoder(
val encoder = MockProtoEncoder(Pep(
JellyOptions.smallGeneralized.withPhysicalType(PhysicalStreamType.GRAPHS)
)
))
val encoded = Graphs1.mrl.flatMap((graphName, triples) => Seq(
encoder.startGraph(graphName).toSeq,
triples.flatMap(triple => encoder.addTripleStatement(triple).toSeq),
Expand All @@ -94,10 +95,10 @@ class ProtoEncoderSpec extends AnyWordSpec, Matchers:

"encode graphs with an external buffer" in {
val buffer = ListBuffer[RdfStreamRow]()
val encoder = MockProtoEncoder(
val encoder = MockProtoEncoder(Pep(
JellyOptions.smallGeneralized.withPhysicalType(PhysicalStreamType.GRAPHS),
false, Some(buffer)
)
))
for (graphName, triples) <- Graphs1.mrl do
val start = encoder.startGraph(graphName)
start.size should be (0)
Expand All @@ -111,19 +112,19 @@ class ProtoEncoderSpec extends AnyWordSpec, Matchers:
}

"not allow to end a graph before starting one" in {
val encoder = MockProtoEncoder(
val encoder = MockProtoEncoder(Pep(
JellyOptions.smallGeneralized.withPhysicalType(PhysicalStreamType.QUADS)
)
))
val error = intercept[RdfProtoSerializationError] {
encoder.endGraph()
}
error.getMessage should include ("Cannot end a delimited graph before starting one")
}

"not allow to use quoted triples as the graph name" in {
val encoder = MockProtoEncoder(
val encoder = MockProtoEncoder(Pep(
JellyOptions.smallGeneralized.withPhysicalType(PhysicalStreamType.GRAPHS)
)
))
val error = intercept[RdfProtoSerializationError] {
encoder.startGraph(TripleNode(
Triple(BlankNode("S"), BlankNode("P"), BlankNode("O"))
Expand All @@ -133,10 +134,10 @@ class ProtoEncoderSpec extends AnyWordSpec, Matchers:
}

"not allow to use namespace declarations if they are not enabled" in {
val encoder = MockProtoEncoder(
val encoder = MockProtoEncoder(Pep(
JellyOptions.smallGeneralized.withPhysicalType(PhysicalStreamType.TRIPLES),
enableNamespaceDeclarations = false,
)
))
val error = intercept[RdfProtoSerializationError] {
encoder.declareNamespace("test", "https://test.org/test/")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package eu.ostrzyciel.jelly.core.helpers

import eu.ostrzyciel.jelly.core.ConverterFactory
import eu.ostrzyciel.jelly.core.{ConverterFactory, ProtoEncoder}
import eu.ostrzyciel.jelly.core.helpers.Mrl.*
import eu.ostrzyciel.jelly.core.proto.v1.{RdfStreamOptions, RdfStreamRow}

Expand All @@ -11,9 +11,5 @@ object MockConverterFactory extends ConverterFactory

override final def decoderConverter = new MockProtoDecoderConverter()

override final def encoder(
options: RdfStreamOptions,
enableNamespaceDeclarations: Boolean,
maybeRowBuffer: Option[mutable.Buffer[RdfStreamRow]],
) =
new MockProtoEncoder(options, enableNamespaceDeclarations, maybeRowBuffer)
override final def encoder(params: ProtoEncoder.Params) =
new MockProtoEncoder(params)
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,9 @@ import scala.collection.mutable

/**
* Mock implementation of ProtoEncoder
* @param options options for this stream
* @param enableNamespaceDeclarations whether to enable namespace declarations
* @param maybeRowBuffer optional buffer for storing stream rows that should go into a stream frame
*/
class MockProtoEncoder(
options: RdfStreamOptions,
enableNamespaceDeclarations: Boolean = false,
maybeRowBuffer: Option[mutable.Buffer[RdfStreamRow]] = None
) extends ProtoEncoder[Node, Triple, Quad, Triple](options, enableNamespaceDeclarations, maybeRowBuffer):
class MockProtoEncoder(params: ProtoEncoder.Params)
extends ProtoEncoder[Node, Triple, Quad, Triple](params):

protected inline def getTstS(triple: Triple) = triple.s
protected inline def getTstP(triple: Triple) = triple.p
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package eu.ostrzyciel.jelly.convert.jena

import eu.ostrzyciel.jelly.core.proto.v1.{RdfStreamOptions, RdfStreamRow}
import eu.ostrzyciel.jelly.core.ConverterFactory
import eu.ostrzyciel.jelly.core.{ConverterFactory, ProtoEncoder}
import org.apache.jena.datatypes.RDFDatatype
import org.apache.jena.graph.{Node, Triple}
import org.apache.jena.sparql.core.Quad

import scala.collection.mutable

object JenaConverterFactory
extends ConverterFactory[JenaProtoEncoder, JenaDecoderConverter, Node, RDFDatatype, Triple, Quad]:

Expand All @@ -19,9 +16,5 @@ object JenaConverterFactory
/**
* @inheritdoc
*/
override final def encoder(
options: RdfStreamOptions,
enableNamespaceDeclarations: Boolean,
maybeRowBuffer: Option[mutable.Buffer[RdfStreamRow]],
): JenaProtoEncoder =
JenaProtoEncoder(options, enableNamespaceDeclarations, maybeRowBuffer)
override final def encoder(params: ProtoEncoder.Params): JenaProtoEncoder =
JenaProtoEncoder(params)
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
package eu.ostrzyciel.jelly.convert.jena

import eu.ostrzyciel.jelly.core.*
import eu.ostrzyciel.jelly.core.proto.v1.{GraphTerm, RdfStreamOptions, RdfStreamRow, SpoTerm}
import eu.ostrzyciel.jelly.core.proto.v1.{GraphTerm, SpoTerm}
import org.apache.jena.datatypes.xsd.XSDDatatype
import org.apache.jena.graph.*
import org.apache.jena.sparql.core.Quad

import scala.collection.mutable

final class JenaProtoEncoder(
options: RdfStreamOptions,
enableNamespaceDeclarations: Boolean,
maybeRowBuffer: Option[mutable.Buffer[RdfStreamRow]] = None,
) extends ProtoEncoder[Node, Triple, Quad, Triple](options, enableNamespaceDeclarations, maybeRowBuffer):
final class JenaProtoEncoder(params: ProtoEncoder.Params)
extends ProtoEncoder[Node, Triple, Quad, Triple](params):

protected inline def getTstS(triple: Triple) = triple.getSubject
protected inline def getTstP(triple: Triple) = triple.getPredicate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,29 @@ import org.scalatest.wordspec.AnyWordSpec
* Test the handling of the many ways to represent the default graph in Jena.
*/
class JenaProtoEncoderSpec extends AnyWordSpec, Matchers, JenaTest:
import ProtoEncoder.Params as Pep

private val encodedDefaultGraph = RdfStreamRow(
RdfGraphStart(RdfDefaultGraph())
)

"JenaProtoEncoder" should {
"encode a null graph node as default graph" in {
val encoder = JenaProtoEncoder(JellyOptions.smallGeneralized, false)
val encoder = JenaProtoEncoder(Pep(JellyOptions.smallGeneralized, false))
val rows = encoder.startGraph(null).toSeq
rows.size should be (2)
rows(1) should be (encodedDefaultGraph)
}

"encode an explicitly named default graph as default graph" in {
val encoder = JenaProtoEncoder(JellyOptions.smallGeneralized, false)
val encoder = JenaProtoEncoder(Pep(JellyOptions.smallGeneralized, false))
val rows = encoder.startGraph(Quad.defaultGraphIRI).toSeq
rows.size should be (2)
rows(1) should be (encodedDefaultGraph)
}

"encode a generated default graph as default graph" in {
val encoder = JenaProtoEncoder(JellyOptions.smallGeneralized, false)
val encoder = JenaProtoEncoder(Pep(JellyOptions.smallGeneralized, false))
val rows = encoder.startGraph(Quad.defaultGraphNodeGenerated).toSeq
rows.size should be (2)
rows(1) should be (encodedDefaultGraph)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package eu.ostrzyciel.jelly.convert.rdf4j

import eu.ostrzyciel.jelly.core.ConverterFactory
import eu.ostrzyciel.jelly.core.proto.v1.{RdfStreamOptions, RdfStreamRow}
import eu.ostrzyciel.jelly.core.{ConverterFactory, ProtoEncoder}
import org.eclipse.rdf4j.model.{Statement, Value}

import scala.collection.mutable

object Rdf4jConverterFactory
extends ConverterFactory[Rdf4jProtoEncoder, Rdf4jDecoderConverter, Value, Rdf4jDatatype, Statement, Statement]:

Expand All @@ -17,9 +14,5 @@ object Rdf4jConverterFactory
/**
* @inheritdoc
*/
override final def encoder(
options: RdfStreamOptions,
enableNamespaceDeclarations: Boolean,
maybeRowBuffer: Option[mutable.Buffer[RdfStreamRow]],
): Rdf4jProtoEncoder =
Rdf4jProtoEncoder(options, enableNamespaceDeclarations, maybeRowBuffer)
override final def encoder(params: ProtoEncoder.Params): Rdf4jProtoEncoder =
Rdf4jProtoEncoder(params)
Loading

0 comments on commit 20e667c

Please sign in to comment.