Skip to content

Commit

Permalink
Add xml ConfigProvider with inbuilt parser (#1065)
Browse files Browse the repository at this point in the history
* 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
afsalthaj committed Feb 22, 2023
1 parent 8b58dbb commit 636a617
Show file tree
Hide file tree
Showing 18 changed files with 657 additions and 4 deletions.
26 changes: 23 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@ addCommandAlias(

addCommandAlias(
"testJVM212",
";zioConfigJVM/test;zioConfigTypesafeJVM/test;zioConfigDerivationJVM/test;zioConfigYamlJVM/test;examplesJVM/test;zioConfigAwsJVM/test;zioConfigZioAwsJVM/test"
";zioConfigJVM/test;zioConfigTypesafeJVM/test;zioConfigDerivationJVM/test;zioConfigYamlJVM/test;examplesJVM/test;zioConfigAwsJVM/test;zioConfigZioAwsJVM/test;zioConfigXmlJVM/test"
)
addCommandAlias(
"testJVM213",
";zioConfigJVM/test;zioConfigTypesafeJVM/test;zioConfigDerivationJVM/test;zioConfigYamlJVM/test;zioConfigRefinedJVM/test;zioConfigMagnoliaJVM/test;examplesJVM/test;zioConfigTypesafeMagnoliaTestsJVM/test;zioConfigAwsJVM/test;zioConfigZioAwsJVM/test"
";zioConfigJVM/test;zioConfigTypesafeJVM/test;zioConfigDerivationJVM/test;zioConfigYamlJVM/test;zioConfigRefinedJVM/test;zioConfigMagnoliaJVM/test;examplesJVM/test;zioConfigTypesafeMagnoliaTestsJVM/test;zioConfigAwsJVM/test;zioConfigZioAwsJVM/test;zioConfigXmlJVM/test"
)
addCommandAlias(
"testJVM3x",
";zioConfigJVM/test;zioConfigTypesafeJVM/test;zioConfigDerivationJVM/test;zioConfigYamlJVM/test;zioConfigAwsJVM/test;zioConfigZioAwsJVM/test"
";zioConfigJVM/test;zioConfigTypesafeJVM/test;zioConfigDerivationJVM/test;zioConfigYamlJVM/test;zioConfigAwsJVM/test;zioConfigZioAwsJVM/test;zioConfigXmlJVM/test"
)

val awsVersion = "1.12.360"
Expand Down Expand Up @@ -91,12 +91,14 @@ lazy val scala211projects =
zioConfigYamlJVM,
docs
)

lazy val scala212projects = Seq[ProjectReference](
zioConfigEnumeratumJVM,
zioConfigCatsJVM,
zioConfigRefinedJVM,
zioConfigMagnoliaJVM,
zioConfigZioAwsJVM,
zioConfigXmlJVM,
examplesJVM
)

Expand All @@ -115,6 +117,7 @@ lazy val scala3projects =
zioConfigScalazJVM,
zioConfigTypesafeJVM,
zioConfigYamlJVM,
zioConfigXmlJVM,
docs
)

Expand Down Expand Up @@ -350,6 +353,23 @@ lazy val zioConfigYaml = crossProject(JVMPlatform)
lazy val zioConfigYamlJVM = zioConfigYaml.jvm
.settings(dottySettings)

lazy val zioConfigXml = crossProject(JVMPlatform)
.in(file("xml"))
.settings(stdSettings("zio-config-xml"))
.settings(crossProjectSettings)
.settings(
libraryDependencies ++= Seq(
"dev.zio" %% "zio-parser" % "0.1.8",
"dev.zio" %% "zio-test" % zioVersion % Test,
"dev.zio" %% "zio-test-sbt" % zioVersion % Test
),
testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework"))
)
.dependsOn(zioConfig % "compile->compile;test->test")

lazy val zioConfigXmlJVM = zioConfigXml.jvm
.settings(dottySettings)

lazy val zioConfigScalaz = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.in(file("scalaz"))
.settings(stdSettings("zio-config-scalaz"))
Expand Down
21 changes: 20 additions & 1 deletion docs/read-from-various-sources.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,23 @@ ConfigProvider.fromHoconString(jsonString)

## Yaml FIle

Similar to Hocon source, the only difference is `ConfigProvider.fromYamlString`
Similar to Hocon source, we have `ConfigProvider.fromYamlString`

```scala
import zio.config.yaml._

ConfigProvider.fromYamlString

```

## Xml String

zio-config can read XML strings. Note that it's experimental with a dead simple native xml parser,
Currently it cannot XML comments, which will be fixed in the near future.

```scala
import zio.config.xml._

ConfigProvider.fromXmlString

```
78 changes: 78 additions & 0 deletions xml/shared/src/main/scala/zio/config/xml/Parsers.scala
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 xml/shared/src/main/scala/zio/config/xml/XmlConfigProvider.scala
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 }))
}

}
49 changes: 49 additions & 0 deletions xml/shared/src/main/scala/zio/config/xml/XmlObject.scala
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
}
50 changes: 50 additions & 0 deletions xml/shared/src/main/scala/zio/config/xml/XmlParser.scala
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)

}
10 changes: 10 additions & 0 deletions xml/shared/src/main/scala/zio/config/xml/package.scala
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 xml/shared/src/test/scala/zio/config/yaml/XmlParserSpec.scala
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)))
}
}
)
}
Loading

0 comments on commit 636a617

Please sign in to comment.