-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fs2-data-xml streaming parser #25
Changes from 10 commits
1ca64f2
2e2935b
32e569e
f92af16
62bfde3
d2426eb
72b7b60
96e38de
be9d87f
208fd19
ceb7dcd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
addSbtPlugin("org.http4s" % "sbt-http4s-org" % "0.14.3") | ||
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.10.0") |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,24 +17,21 @@ | |
package org.http4s | ||
package scalaxml | ||
|
||
import cats.data.EitherT | ||
import cats.effect.Async | ||
import cats.effect.Concurrent | ||
import cats.syntax.all._ | ||
import fs2.Stream | ||
import fs2.data.xml.XmlEvent | ||
import fs2.data.xml.XmlException | ||
import fs2.data.xml.scalaXml._ | ||
import org.http4s.Charset.`UTF-8` | ||
import org.http4s.headers.`Content-Type` | ||
|
||
import java.io.ByteArrayInputStream | ||
import java.io.StringWriter | ||
import javax.xml.parsers.SAXParserFactory | ||
import scala.util.control.NonFatal | ||
import scala.xml.Document | ||
import scala.xml.Elem | ||
import scala.xml.InputSource | ||
import scala.xml.SAXParseException | ||
import scala.xml.XML | ||
|
||
trait ElemInstances { | ||
protected def saxFactory: SAXParserFactory | ||
|
||
implicit def xmlEncoder[F[_]](implicit charset: Charset = `UTF-8`): EntityEncoder[F, Elem] = | ||
EntityEncoder | ||
|
@@ -46,50 +43,29 @@ trait ElemInstances { | |
} | ||
.withContentType(`Content-Type`(MediaType.application.xml).withCharset(charset)) | ||
|
||
implicit def xmlEvents[F[_]](implicit F: Concurrent[F]): EntityDecoder[F, Stream[F, XmlEvent]] = | ||
EntityDecoder.decodeBy(MediaType.text.xml, MediaType.text.html, MediaType.application.xml) { | ||
msg => | ||
DecodeResult.successT(msg.bodyText.through(fs2.data.xml.events(includeComments = true))) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is necessary to make the new round-trip test pass. But is it actually what we want? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I conflated normalization in the generator with normalization in the test in a way I probably shouldn't have. I guess the question is whether comments are part of normalized output. I think it belongs, perhaps with an option to turn it off. |
||
} | ||
|
||
/** Handles a message body as XML. | ||
* | ||
* TODO Not an ideal implementation. Would be much better with an asynchronous XML parser, such as Aalto. | ||
* | ||
* @return an XML element | ||
* @return an XML [[Document]] | ||
*/ | ||
@deprecated("Blocks. Use xmlDecoder with an Async constraint.", "0.23.12") | ||
def xml[F[_]](implicit F: Concurrent[F]): EntityDecoder[F, Elem] = { | ||
import EntityDecoder._ | ||
decodeBy(MediaType.text.xml, MediaType.text.html, MediaType.application.xml) { msg => | ||
val source = new InputSource() | ||
msg.charset.foreach(cs => source.setEncoding(cs.nioCharset.name)) | ||
|
||
collectBinary(msg).flatMap[DecodeFailure, Elem] { chunk => | ||
source.setByteStream(new ByteArrayInputStream(chunk.toArray)) | ||
val saxParser = saxFactory.newSAXParser() | ||
try DecodeResult.successT[F, Elem](XML.loadXML(source, saxParser)) | ||
catch { | ||
case e: SAXParseException => | ||
DecodeResult.failureT(MalformedMessageBodyFailure("Invalid XML", Some(e))) | ||
case NonFatal(e) => DecodeResult(F.raiseError[Either[DecodeFailure, Elem]](e)) | ||
} | ||
implicit def xml[F[_]](implicit F: Concurrent[F]): EntityDecoder[F, Document] = | ||
xmlEvents.flatMapR { events => | ||
DecodeResult { | ||
events | ||
.through(fs2.data.xml.dom.documents) | ||
.head | ||
.compile | ||
.lastOrError | ||
.map(Either.right[MalformedMessageBodyFailure, Document](_)) | ||
.recover { case ex: XmlException => | ||
Left(MalformedMessageBodyFailure("Invalid XML", Some(ex))) | ||
} | ||
.widen | ||
} | ||
} | ||
} | ||
|
||
implicit def xmlDecoder[F[_]](implicit F: Async[F]): EntityDecoder[F, Elem] = { | ||
import EntityDecoder._ | ||
decodeBy(MediaType.text.xml, MediaType.text.html, MediaType.application.xml) { msg => | ||
val source = new InputSource() | ||
msg.charset.foreach(cs => source.setEncoding(cs.nioCharset.name)) | ||
|
||
collectBinary(msg).flatMap[DecodeFailure, Elem] { chunk => | ||
source.setByteStream(new ByteArrayInputStream(chunk.toArray)) | ||
val saxParser = saxFactory.newSAXParser() | ||
EitherT( | ||
F.blocking(XML.loadXML(source, saxParser)) | ||
.map(Either.right[DecodeFailure, Elem](_)) | ||
.recover { case e: SAXParseException => | ||
Left(MalformedMessageBodyFailure("Invalid XML", Some(e))) | ||
} | ||
) | ||
} | ||
} | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I couldn't decide whether or not to bump the base version. We can make this change binary-compatibly, it's more about semantics.