diff --git a/morphir-ir/shared/src/main/scala/zio/morphir/ir/Name.scala b/morphir-ir/shared/src/main/scala/zio/morphir/ir/Name.scala index e288153f..0541bf3c 100644 --- a/morphir-ir/shared/src/main/scala/zio/morphir/ir/Name.scala +++ b/morphir-ir/shared/src/main/scala/zio/morphir/ir/Name.scala @@ -47,6 +47,8 @@ final case class Name private (toList: List[String]) extends AnyVal { self => def mkString(f: String => String)(sep: String): String = toList.map(f).mkString(sep) + def toUpperCase: String = mkString(part => part.toUpperCase)("") + def toLowerCase: String = mkString(part => part.toLowerCase)("") @@ -69,6 +71,7 @@ final case class Name private (toList: List[String]) extends AnyVal { self => .mkString("") } + object Name { val empty: Name = Name(Nil) diff --git a/morphir-ir/shared/src/main/scala/zio/morphir/ir/Path.scala b/morphir-ir/shared/src/main/scala/zio/morphir/ir/Path.scala index 4a5b2a9a..8b05f84b 100644 --- a/morphir-ir/shared/src/main/scala/zio/morphir/ir/Path.scala +++ b/morphir-ir/shared/src/main/scala/zio/morphir/ir/Path.scala @@ -2,6 +2,8 @@ package zio.morphir.ir import zio.Chunk import zio.morphir.ir.PackageModule.PackageAndModulePath +import scala.annotation.tailrec + final case class Path(segments: Chunk[Name]) { self => /** Constructs a new path by combining this path with the given name. */ @@ -18,8 +20,43 @@ final case class Path(segments: Chunk[Name]) { self => def isEmpty: Boolean = segments.isEmpty def zip(other: Path): (Path, Path) = (self, other) + def toList: List[Name] = segments.toList + + def toString(f: Name => String, separator: String): String = + segments.map(f).mkString(separator) + + /** Checks if this path is a prefix of provided path */ + def isPrefixOf(path: Path): Boolean = Path.isPrefixOf(self, path) } object Path { val empty: Path = Path(Chunk.empty) + + private def wrap(value: List[Name]): Path = Path(Chunk.fromIterable(value)) + + def fromString(str: String): Path = { + val separatorRegex = """[^\w\s]+""".r + wrap(separatorRegex.split(str).map(Name.fromString).toList) + } + + def toString(f: Name => String, separator: String, path: Path): String = + path.toString(f, separator) + + @inline def fromList(names: List[Name]): Path = wrap(names) + + @inline def toList(path: Path): List[Name] = path.segments.toList + + /** Checks if the first provided path is a prefix of the second path */ + @tailrec + def isPrefixOf(prefix: Path, path: Path): Boolean = (prefix.toList, path.toList) match { + case (Nil, _) => true + case (_, Nil) => false + case (prefixHead :: prefixTail, pathHead :: pathTail) => + if (prefixHead == pathHead) + isPrefixOf( + Path.fromList(prefixTail), + Path.fromList(pathTail) + ) + else false + } } diff --git a/morphir-ir/shared/src/main/scala/zio/morphir/ir/QName.scala b/morphir-ir/shared/src/main/scala/zio/morphir/ir/QName.scala index 2486e072..c3412658 100644 --- a/morphir-ir/shared/src/main/scala/zio/morphir/ir/QName.scala +++ b/morphir-ir/shared/src/main/scala/zio/morphir/ir/QName.scala @@ -4,13 +4,26 @@ final case class QName(modulePath: Path, localName: Name) { @inline def toTuple: (Path, Name) = (modulePath, localName) override def toString: String = - if (modulePath.isEmpty) localName.toString - else modulePath.toString + "." + localName.toString + modulePath.toString(Name.toTitleCase, ".") + ":" + localName.toCamelCase + } object QName { + def toTuple(qName: QName): (Path, Name) = qName.toTuple def fromTuple(tuple: (Path, Name)): QName = QName(tuple._1, tuple._2) + def fromName(modulePath: Path, localName: Name): QName = QName(modulePath, localName) + def getLocalName(qname: QName): Name = qname.localName def getModulePath(qname: QName): Path = qname.modulePath + + def toString(qName: QName): String = qName.toString + + def fromString(str: String): Option[QName] = { + str.split(":") match { + case Array(packageNameString, localNameString) => + Some(QName(Path.fromString(packageNameString), Name.fromString(localNameString))) + case _ => None + } + } } diff --git a/morphir-ir/shared/src/test/scala/zio/morphir/ir/NameSpec.scala b/morphir-ir/shared/src/test/scala/zio/morphir/ir/NameSpec.scala index 0897803f..5d38521e 100644 --- a/morphir-ir/shared/src/test/scala/zio/morphir/ir/NameSpec.scala +++ b/morphir-ir/shared/src/test/scala/zio/morphir/ir/NameSpec.scala @@ -2,6 +2,7 @@ package zio.morphir.ir import zio.morphir.ir.testing.MorphirBaseSpec import zio.test.* + object NameSpec extends MorphirBaseSpec { def spec = suite("Name")( suite("Create a Name from a string and check that:")( diff --git a/morphir-ir/shared/src/test/scala/zio/morphir/ir/PathSpec.scala b/morphir-ir/shared/src/test/scala/zio/morphir/ir/PathSpec.scala index d0701831..53bfa66b 100644 --- a/morphir-ir/shared/src/test/scala/zio/morphir/ir/PathSpec.scala +++ b/morphir-ir/shared/src/test/scala/zio/morphir/ir/PathSpec.scala @@ -6,11 +6,74 @@ import testing.MorphirBaseSpec object PathSpec extends MorphirBaseSpec { def spec = suite("Path")( - test("It can be constructed from names")( - assertTrue( - Name("Org") / Name("Finos") == Path(Chunk(Name("Org"), Name("Finos"))), - Name("Alpha") / Name("Beta") / Name("Gamma") == Path(Chunk(Name("Alpha"), Name("Beta"), Name("Gamma"))) + suite("Creating a Path from a String")( + test("It can be constructed from a simple string") { + assertTrue(Path.fromString("Person") == Path(Chunk(Name.fromString("person")))) + }, + test("It can be constructed when given a dotted string") { + assertTrue( + Path.fromString("blog.Author") == Path( + Chunk(Name.fromList(List("blog")), Name.fromList(List("author"))) + ) + ) + } + ), + suite("Transforming a Path into a String")( + test("Paths with period and TitleCase") { + val input = Path( + Chunk( + Name("foo", "bar"), + Name("baz") + ) + ) + assertTrue(Path.toString(Name.toTitleCase, ".", input) == "FooBar.Baz") + }, + test("Paths with slash and Snake_Case") { + val input = Path( + Chunk( + Name("foo", "bar"), + Name("baz") + ) + ) + assertTrue(Path.toString(Name.toSnakeCase, "/", input) == "foo_bar/baz") + } + ), + suite("Transforming a Path into list of Names")( + test("It can be constructed using toList") { + assertTrue( + Path.toList(Path(Chunk(Name("Com", "Example"), Name("Hello", "World")))) + == List(Name("Com", "Example"), Name("Hello", "World")) + ) + } + ), + suite("Creating a Path from a Name")( + test("It can be constructed from names")( + assertTrue( + Name("Org") / Name("Finos") == Path(Chunk(Name("Org"), Name("Finos"))), + Name("Alpha") / Name("Beta") / Name("Gamma") == Path(Chunk(Name("Alpha"), Name("Beta"), Name("Gamma"))) + ) ) + ), + suite("Checking if one Path is a prefix of another should:")( + test("""Return true: Given path is "foo/bar" and prefix is "foo" """) { + val sut = Path.fromString("foo/bar") + val prefix = Path.fromString("foo") + val x = Path.isPrefixOf(prefix = prefix, path = sut) + assertTrue(x) + }, + test("""Return false: Given path is "foo/foo" and prefix is "bar" """) { + val sut = Path.fromString("foo/foo") + val prefix = Path.fromString("bar") + + val x = Path.isPrefixOf(prefix = prefix, path = sut) + assertTrue(!x) + }, + test("""Return true: Given equal paths""") { + val sut = Path.fromString("foo/bar/baz") + val prefix = sut + val x = Path.isPrefixOf(prefix = prefix, path = sut) + assertTrue(x) + } ) ) } diff --git a/morphir-ir/shared/src/test/scala/zio/morphir/ir/QNameSpec.scala b/morphir-ir/shared/src/test/scala/zio/morphir/ir/QNameSpec.scala new file mode 100644 index 00000000..48dd0ea5 --- /dev/null +++ b/morphir-ir/shared/src/test/scala/zio/morphir/ir/QNameSpec.scala @@ -0,0 +1,51 @@ +package zio.morphir.ir + +import zio.test.* +import zio.morphir.ir.testing.MorphirBaseSpec + +object QNameSpec extends MorphirBaseSpec { + def spec = suite("QName")( + suite("Creating a tuple from QName")( + test("toTuple should provide the Path and Name as a tuple") { + val path = Path.fromString("ice.cream") + val name = Name.fromString("float") + val expected = (path, name) + assertTrue(QName(path, name).toTuple == expected) + } + ), + suite("Creating a QName")( + test("Creating a QName with a tuple") { + val path = Path.fromString("friday") + val name = Name.fromString("night") + assertTrue(QName.fromTuple((path, name)) == QName(path, name)) + }, + test("Creating a QName from a name") { + val path = Path.fromString("blog.Author") + val name = Name.fromString("book") + assertTrue(QName.fromName(path, name) == QName(path, name)) + } + ), + suite("Fetching values from QName")( + test("localName and path") { + val path = Path.fromString("path") + val name = Name.fromString("name") + assertTrue(QName.getLocalName(QName(path, name)) == name) + assertTrue(QName.getModulePath(QName(path, name)) == path) + } + ), + suite("QName and Strings")( + test("Create String from QName") { + val path = Path.fromString("front.page") + val name = Name.fromString("dictionary words") + assertTrue(QName(path, name).toString == "Front.Page:dictionaryWords") + }, + test("Create QName from String") { + val str = "Proper.Path:name" + assertTrue(QName.fromString(str) == Some(QName(Path.fromString("Proper.Path"), Name.fromString("name")))) + + val str2 = "invalidpathname" + assertTrue(QName.fromString(str2) == None) + } + ) + ) +}