Skip to content
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

XML and experimental SOAP support #543

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,19 @@ lazy val csv = Build
)
.dependsOn(openapi)

lazy val xml = Build
.defineProject("xml")
.settings(
libraryDependencies ++= scalaXml
)
.dependsOn(openapi)

lazy val soap = Build
.defineProject("soap")
.settings(
)
.dependsOn(xml)

lazy val root = (project in file("."))
.settings(
name := "chopsticks",
Expand All @@ -293,6 +306,8 @@ lazy val root = (project in file("."))
metric,
openapi,
csv,
xml,
soap,
alertmanager,
prometheus,
zioGrpcCommon,
Expand Down
25 changes: 2 additions & 23 deletions chopsticks-csv/src/main/scala/dev/chopsticks/csv/CsvEncoder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import dev.chopsticks.openapi.{OpenApiParsedAnnotations, OpenApiSumTypeSerDeStra
import dev.chopsticks.openapi.common.{ConverterCache, OpenApiConverterUtils}
import org.apache.commons.text.StringEscapeUtils
import zio.schema.{Schema, StandardType, TypeId}
import zio.{Chunk, ChunkBuilder}
import zio.Chunk
import zio.schema.Schema.{Field, Primitive}

import java.time.{
Expand Down Expand Up @@ -58,27 +58,6 @@ final case class CsvEncodingResult(headers: Chunk[String], rows: Chunk[Chunk[Str

trait CsvEncoder[A] {
self =>
def encodeSeq(values: Iterable[A]): CsvEncodingResult = {
val encodedValues = Chunk.fromIterable(values).map(v => encode(v))
val headers = encodedValues
.foldLeft(mutable.SortedSet.empty[String]) { case (acc, next) =>
acc ++ next.keys
}
val singleRowBuilder = ChunkBuilder.make[String](headers.size)
val rows = encodedValues
.foldLeft(ChunkBuilder.make[Chunk[String]](values.size)) { case (acc, next) =>
singleRowBuilder.clear()
val row = headers
.foldLeft(singleRowBuilder) { case (rowBuilder, header) =>
rowBuilder += next.getOrElse(header, "")
}
.result()
acc += row
}
.result()

CsvEncodingResult(Chunk.fromIterable(headers), Chunk.fromIterable(rows))
}

def encode(value: A): mutable.LinkedHashMap[String, String] =
encode(value, columnName = None, mutable.LinkedHashMap.empty)
Expand Down Expand Up @@ -464,7 +443,7 @@ object CsvEncoder {
val diff = discriminator.mapping.values.toSet.diff(encodersByName.keySet)
if (diff.nonEmpty) {
throw new RuntimeException(
s"Cannot derive CsvEncoder for ${enumAnnotations.entityName.getOrElse("-")}, because mapping and decoders don't match. Diff=$diff."
s"Cannot derive CsvEncoder for ${id.name}, because mapping and decoders don't match. Diff=$diff."
)
}
new CsvEncoder[A] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,25 @@ object OpenApiConverterUtils {
}
}
}

private[chopsticks] def isSeq(schema: Schema[_]): Boolean = {
schema match {
case _: Schema.Sequence[_, _, _] => true
case _: Schema.Set[_] => true
case _: Schema.Primitive[_] => false
case o: Schema.Optional[_] => isSeq(o.schema)
case t: Schema.Transform[_, _, _] => isSeq(t.schema)
case l: Schema.Lazy[_] => isSeq(l.schema)
case _: Schema.Record[_] => false
case _: Schema.Enum[_] => false
case _: Schema.Map[_, _] => false
case _: Schema.Either[_, _] => false
case _: Schema.Tuple2[_, _] => false
case _: Schema.Fail[_] => false
case _: Schema.Fallback[_, _] =>
throw new IllegalArgumentException("Fallback schema is not supported")
case _: Schema.Dynamic =>
throw new IllegalArgumentException("Dynamic schema is not supported")
}
}
}
177 changes: 177 additions & 0 deletions chopsticks-soap/src/main/scala/dev/chopsticks/soap/wsdl/Wsdl.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package dev.chopsticks.soap.wsdl

import dev.chopsticks.soap.XsdSchema
import dev.chopsticks.soap.wsdl.WsdlParsingError.{SoapEnvelopeParsing, XmlParsing}
import dev.chopsticks.xml.{XmlDecoder, XmlDecoderError, XmlEncoder}
import zio.Chunk

import scala.collection.immutable.{ListMap, ListSet}
import scala.util.{Failure, Success, Try}
import scala.xml.{Elem, PrefixedAttribute}

sealed trait WsdlParsingError extends Product with Serializable
object WsdlParsingError {
final case class UnknownAction(received: String, supported: Iterable[String]) extends WsdlParsingError
final case class XmlParsing(message: String, internalException: Throwable) extends WsdlParsingError
final case class SoapEnvelopeParsing(message: String) extends WsdlParsingError
final case class XmlDecoder(errors: List[XmlDecoderError]) extends WsdlParsingError
}

// follows subset of the WSDL 1.1 spec
final case class Wsdl(
definitions: WsdlDefinitions,
portTypeName: String,
bindingName: String,
operations: ListSet[WsdlOperation]
) {
private lazy val operationsByName = operations.iterator.map(op => op.name -> op).to(ListMap)

def addOperation(operation: WsdlOperation): Wsdl = {
copy(operations = this.operations + operation)
}

def parseBodyPart1(
soapAction: String,
body: String
): Either[WsdlParsingError, (WsdlOperation, Any)] = {
operationsByName.get(soapAction) match {
case None => Left(WsdlParsingError.UnknownAction(soapAction, operationsByName.keys))
case Some(operation) =>
Try(xml.XML.loadString(body)) match {
case Failure(e) => Left(XmlParsing(s"Provided XML is not valid.", e))
case Success(xml) =>
xml.child.collectFirst { case elem: Elem if elem.label == "Body" => elem } match {
case None => Left(SoapEnvelopeParsing("Body element not found in the provided XML."))
case Some(bodyElem) =>
bodyElem.child.collectFirst { case elem: Elem if elem.label == operation.name => elem } match {
case None =>
Left(SoapEnvelopeParsing(s"${operation.name} element not found within the soapenv:Body."))
case Some(operationElem) =>
operation.input.parts.headOption match {
case None => Left(SoapEnvelopeParsing(s"No input parts found for operation ${operation.name}."))
case Some(wsdlPart) =>
operationElem.child.collectFirst { case e: Elem if e.label == wsdlPart.name => e } match {
case None =>
Left(SoapEnvelopeParsing(
s"${wsdlPart.name} not found for operation ${operation.name} in the received XML."
))
case Some(partElem) =>
wsdlPart.xmlDecoder.parse(partElem.child) match {
case Left(errors) => Left(WsdlParsingError.XmlDecoder(errors))
case Right(value) => Right((operation, value))
}
}
}
}
}
}
}
}

def serializeResponsePart1[A: XsdSchema](operation: WsdlOperation, response: A): Elem = {
if (operation.output.parts.size != 1) {
throw new RuntimeException(
s"Only single part output is supported. Got ${operation.output.parts.size} parts instead."
)
}
if (operation.output.parts.head.xsdSchema != implicitly[XsdSchema[A]]) {
throw new RuntimeException("XsdSchema[A] must match the schema of the part.")
}
val part = operation.output.parts.head.asInstanceOf[WsdlMessagePart[A]]
Elem(
"soapenv",
"Envelope",
new PrefixedAttribute("xmlns", "soapenv", "http://schemas.xmlsoap.org/soap/envelope/", scala.xml.Null),
scala.xml.TopScope,
minimizeEmpty = true,
Elem(
"soapenv",
"Header",
scala.xml.Null,
scala.xml.TopScope,
minimizeEmpty = true
),
Elem(
"soapenv",
"Body",
scala.xml.Null,
scala.xml.TopScope,
minimizeEmpty = true,
Elem(
definitions.custom.key,
part.xsdSchema.name,
new PrefixedAttribute(
"xmlns",
definitions.custom.key,
definitions.targetNamespace,
new PrefixedAttribute("soapenv", "encodingStyle", "http://schemas.xmlsoap.org/soap/encoding/", xml.Null)
),
scala.xml.TopScope,
minimizeEmpty = true,
Elem(
null,
part.name,
xml.Null,
xml.TopScope,
minimizeEmpty = true,
part.xmlEncoder.encode(response): _*
)
)
)
)
}
}

object Wsdl {
def withDefinitions(
targetNamespace: String,
portTypeName: String,
bindingName: String,
definition: WsdlDefinition
): Wsdl = {
Wsdl(
definitions = WsdlDefinitions(
targetNamespace = targetNamespace,
custom = definition
),
portTypeName = portTypeName,
bindingName = bindingName,
operations = ListSet.empty
)
}
}

final case class WsdlDefinitions(
targetNamespace: String,
xmlns: WsdlDefinitionAddress = WsdlDefinition.wsdl.address,
wsdl: WsdlDefinitionAddress = WsdlDefinition.wsdl.address,
wsdlsoap: WsdlDefinitionAddress = WsdlDefinition.wsdlsoap.address,
xsd: WsdlDefinitionAddress = WsdlDefinition.xsd.address,
custom: WsdlDefinition
) {
lazy val defs = Chunk[WsdlDefinition](
WsdlDefinition(WsdlDefinition.wsdl.key, wsdl),
WsdlDefinition(WsdlDefinition.wsdlsoap.key, wsdlsoap),
WsdlDefinition(WsdlDefinition.xsd.key, xsd),
custom
)
}

final case class WsdlDefinition(key: String, address: WsdlDefinitionAddress)
object WsdlDefinition {
val wsdl = WsdlDefinition("wsdl", WsdlDefinitionAddress("http://schemas.xmlsoap.org/wsdl/"))
val wsdlsoap = WsdlDefinition("wsdlsoap", WsdlDefinitionAddress("http://schemas.xmlsoap.org/wsdl/soap/"))
val xsd = WsdlDefinition("xsd", WsdlDefinitionAddress("http://www.w3.org/2001/XMLSchema"))
}

final case class WsdlDefinitionAddress(value: String) extends AnyVal

final case class WsdlMessage(name: String, parts: ListSet[WsdlMessagePart[_]])

final case class WsdlMessagePart[A](name: String)(implicit
val xmlEncoder: XmlEncoder[A],
val xmlDecoder: XmlDecoder[A],
val xsdSchema: XsdSchema[A]
)

final case class WsdlOperation(name: String, input: WsdlMessage, output: WsdlMessage)
Loading
Loading