-
Notifications
You must be signed in to change notification settings - Fork 111
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add xml ConfigProvider with inbuilt parser (#1065)
* Fix build errors, captured only in ci * Fix publish errors * Fix publish errors * Fix publish errors * Add xml provider * Add xml provider * Update docs * Add xml module to build release * Fix scaladocs
- Loading branch information
Showing
18 changed files
with
657 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
package zio.config.xml | ||
|
||
import zio.Chunk | ||
import zio.config.xml.XmlObject.Text | ||
import zio.parser.Parser | ||
|
||
object Parsers { | ||
|
||
lazy private[config] val UnicodeEmptyCharacters: Chunk[Char] = | ||
Chunk( | ||
'\t', | ||
'\r', | ||
' ', | ||
'\n' | ||
) | ||
|
||
lazy private[config] val stringLiteral: Parser[String, Char, String] = | ||
Parser | ||
.charIn('"') | ||
.zip(Parser.charNotIn('"').repeat) | ||
.zip(Parser.charIn('"')) | ||
.map { case (_, b, _) => b.mkString } | ||
|
||
lazy private[config] val nonWS: Parser[String, Char, Chunk[Char]] = | ||
Parser.charNotIn(UnicodeEmptyCharacters: _*).repeat | ||
|
||
lazy private[config] val ws: Parser[String, Char, Unit] = | ||
Parser.charIn(UnicodeEmptyCharacters: _*).repeat0.unit | ||
|
||
lazy private[config] val tagIdentifier: Parser[String, Char, String] = { | ||
val invalid = | ||
Chunk('<', '>') ++ UnicodeEmptyCharacters | ||
|
||
Parser.charNotIn(invalid: _*).repeat.map(_.mkString.trim) | ||
} | ||
|
||
lazy private[config] val textParser: Parser[String, Char, Text] = { | ||
val invalid = | ||
Chunk('<', '>', '=') | ||
|
||
Parser.charNotIn(invalid: _*).repeat.map(s => Text(s.mkString.trim)) | ||
} | ||
|
||
lazy private[config] val attrKeyParser: Parser[String, Char, String] = { | ||
val invalid = | ||
Chunk('=') ++ UnicodeEmptyCharacters | ||
Parser | ||
.charNotIn(invalid: _*) | ||
.repeat | ||
.map(v => v.mkString.trim) | ||
} | ||
|
||
lazy private[config] val attributeValueParser: Parser[String, Char, String] = | ||
stringLiteral | ||
|
||
lazy private[config] val attributeParser: Parser[String, Char, (String, String)] = | ||
attrKeyParser | ||
.zip(ws) | ||
.zip(Parser.charIn('=').unit) | ||
.zip(ws) | ||
.zip(attributeValueParser) | ||
|
||
lazy private[config] val openAngular: Parser[String, Char, Unit] = | ||
Parser.charIn('<').unit | ||
|
||
lazy private[config] val closedAngular: Parser[String, Char, Unit] = | ||
Parser.charIn('>').unit | ||
|
||
lazy private[config] val closedTag: Parser[String, Char, (Char, Char, String)] = | ||
Parser | ||
.charIn('<') | ||
.zip(Parser.charIn('/')) | ||
.zip(ws) | ||
.zip(tagIdentifier) | ||
.zip(ws) | ||
.zip(closedAngular) | ||
|
||
} |
22 changes: 22 additions & 0 deletions
22
xml/shared/src/main/scala/zio/config/xml/XmlConfigProvider.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package zio.config.xml | ||
|
||
import com.github.ghik.silencer.silent | ||
import zio.ConfigProvider | ||
import zio.config.IndexedFlat.ConfigPath | ||
import zio.config._ | ||
|
||
@silent("Unused import") | ||
object XmlConfigProvider { | ||
|
||
/** | ||
* Retrieve a `ConfigProvider` from xml string. | ||
*/ | ||
def fromXmlString(string: String): ConfigProvider = | ||
XmlParser.parse(string) match { | ||
case Left(value) => | ||
throw new Exception(s"Failed to parse xml string. Please make sure the format is correct. ${value}") | ||
case Right(value) => | ||
ConfigProvider.fromIndexedMap(value.flattened.map({ case (k, v) => ConfigPath.toPath(k).mkString(".") -> v })) | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package zio.config.xml | ||
|
||
import zio.Chunk | ||
import zio.config.IndexedFlat.KeyComponent | ||
|
||
// To be moved to zio-config | ||
sealed trait XmlObject { | ||
|
||
def flattened: Map[Chunk[KeyComponent], String] = { | ||
def go( | ||
xmlObject: XmlObject, | ||
path: Chunk[KeyComponent] | ||
): Map[Chunk[KeyComponent], String] = | ||
xmlObject match { | ||
case XmlObject.Text(value) => Map(path -> value) | ||
case XmlObject.TagElement(name, attributes, children) => | ||
val parentNewPath = path ++ Chunk(KeyComponent.KeyName(name)) | ||
|
||
val attributesMap: Map[Chunk[KeyComponent], String] = | ||
attributes.map { case (key, value) => | ||
val subNewPath = | ||
parentNewPath ++ Chunk(KeyComponent.KeyName(key)) | ||
subNewPath -> value | ||
}.toMap | ||
|
||
val childrenMap = children | ||
.map(go(_, parentNewPath)) | ||
.reduceOption(_ ++ _) | ||
.getOrElse(Map.empty) | ||
|
||
attributesMap ++ childrenMap | ||
} | ||
|
||
go(this, Chunk.empty) | ||
|
||
} | ||
} | ||
|
||
object XmlObject { | ||
type Attribute = (String, String) | ||
|
||
final case class Text(value: String) extends XmlObject | ||
|
||
final case class TagElement( | ||
name: String, | ||
attributes: Chunk[Attribute], | ||
children: Chunk[XmlObject] | ||
) extends XmlObject | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package zio.config.xml | ||
|
||
import zio.Chunk | ||
import zio.config.xml.Parsers._ | ||
import zio.config.xml.XmlObject.TagElement | ||
import zio.parser.Parser | ||
|
||
object XmlParser { | ||
|
||
lazy private[config] val tagContent: Parser[String, Char, Option[Chunk[XmlObject]]] = | ||
xmlParser.repeat | ||
.orElseEither(textParser.zip(ws).map(Chunk(_))) | ||
.map(_.merge) | ||
.optional | ||
|
||
lazy private val xmlParser: Parser[ | ||
String, | ||
Char, | ||
XmlObject | ||
] = | ||
ws.zip(openAngular) | ||
.zip(ws) | ||
.zip(tagIdentifier) | ||
.zip(ws) | ||
.zip(attributeParser.zip(ws).repeat0) | ||
.zip(ws) | ||
.zip(closedAngular) | ||
.zip(ws) | ||
.zip(tagContent) | ||
.zip(ws) | ||
.zip(closedTag) | ||
.zip(ws) | ||
.transformEither { case (name, attributes, xmlObject, (_, _, closedTag)) => | ||
if (name == closedTag) { | ||
Right( | ||
TagElement( | ||
name, | ||
attributes, | ||
Chunk.fromIterable(xmlObject.toList).flatten | ||
) | ||
) | ||
} else { | ||
Left(s"Closed tag $closedTag is not the same as open tag $name") | ||
} | ||
} | ||
|
||
private[config] def parse(string: String): Either[Parser.ParserError[String], XmlObject] = | ||
xmlParser.parseString(string) | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package zio.config | ||
|
||
import zio._ | ||
|
||
package object xml { | ||
implicit class FromConfigSourceXml(c: ConfigProvider.type) { | ||
def fromYamlString(xml: String): ConfigProvider = | ||
XmlConfigProvider.fromXmlString(xml) | ||
} | ||
} |
55 changes: 55 additions & 0 deletions
55
xml/shared/src/test/scala/zio/config/yaml/XmlParserSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package zio.config.yaml | ||
|
||
import zio.Scope | ||
import zio.config.xml.XmlParser | ||
import zio.config.yaml.generators.{Space, WhiteSpacedXml} | ||
import zio.test.Assertion._ | ||
import zio.test._ | ||
|
||
object XmlParserSpec extends ZIOSpecDefault { | ||
|
||
def spec: Spec[Environment with TestEnvironment with Scope, Any] = | ||
suite("Xml parser spec")( | ||
test("test xml with zero children and with zero attributes") { | ||
check( | ||
WhiteSpacedXml.gen(0, 0).map(_.emptyChildren).noShrink, | ||
Space.gen.noShrink | ||
) { (simpleXml, space) => | ||
val config = | ||
simpleXml.printWith(space) | ||
|
||
val parsed = XmlParser.parse(config) | ||
|
||
assert(parsed)(equalTo(Right(simpleXml.toXmlObject))) | ||
} | ||
}, | ||
test("test xml with attributes with no children") { | ||
check( | ||
WhiteSpacedXml.gen(1, 10).map(_.emptyChildren).noShrink, | ||
Space.gen.noShrink | ||
) { (xmlWithAttributes, space) => | ||
val config = | ||
xmlWithAttributes.printWith(space) | ||
|
||
val parsed = XmlParser.parse(config) | ||
|
||
assert(parsed)(equalTo(Right(xmlWithAttributes.toXmlObject))) | ||
} | ||
}, | ||
// The round trip test that test any XML! | ||
test("test any xml with or without attributes, with or without children") { | ||
check( | ||
WhiteSpacedXml.gen(1, 10).noShrink, | ||
Space.gen.noShrink | ||
) { (anyXml, space) => | ||
val config = | ||
anyXml.printWith(space) | ||
|
||
val parsed = XmlParser.parse(config) | ||
val expected = anyXml.toXmlObject | ||
|
||
assert(parsed)(equalTo(Right(expected))) | ||
} | ||
} | ||
) | ||
} |
Oops, something went wrong.