diff --git a/core/src/com/morganstanley/morphir/ir/AccessControl.scala b/core/src/com/morganstanley/morphir/ir/AccessControl.scala deleted file mode 100644 index f9682fad..00000000 --- a/core/src/com/morganstanley/morphir/ir/AccessControl.scala +++ /dev/null @@ -1,13 +0,0 @@ -package com.morganstanley.morphir.ir - -object AccessControl { - sealed trait AccessControlled[+A] - case class Public[+A] private (value:A) extends AccessControlled[A] - case class Private[+A] private(value:A) extends AccessControlled[A] - - def `public`[A](value:A):Public[A] = - Public(value) - - def `private`[A](value:A):Private[A] = - Private(value) -} \ No newline at end of file diff --git a/core/src/com/morganstanley/morphir/ir/AccessControlled.scala b/core/src/com/morganstanley/morphir/ir/AccessControlled.scala new file mode 100644 index 00000000..ec93dc8f --- /dev/null +++ b/core/src/com/morganstanley/morphir/ir/AccessControlled.scala @@ -0,0 +1,31 @@ +package com.morganstanley.morphir.ir +sealed trait AccessControlled[+A] { + + def withPublicAccess: Option[A] = this match { + case AccessControlled.Public(v) => Some(v) + case _ => None + } + + def withPrivateAccess: A = this match { + case AccessControlled.Public(v) => v + case AccessControlled.Private(v) => v + } +} + +object AccessControlled { + + case class Public[+A] private (value: A) extends AccessControlled[A] + case class Private[+A] private (value: A) extends AccessControlled[A] + + def `public`[A](value: A): Public[A] = + Public(value) + + def `private`[A](value: A): Private[A] = + Private(value) + + @inline def withPublicAccess[A](ac: AccessControlled[A]): Option[A] = + ac.withPublicAccess + + @inline def withPrivateAccess[A](ac: AccessControlled[A]): A = + ac.withPrivateAccess +} diff --git a/core/src/com/morganstanley/morphir/ir/FQName.scala b/core/src/com/morganstanley/morphir/ir/FQName.scala new file mode 100644 index 00000000..ab88e90a --- /dev/null +++ b/core/src/com/morganstanley/morphir/ir/FQName.scala @@ -0,0 +1,9 @@ +package com.morganstanley.morphir.ir + +case class FQName(packagePath: Path, modulePath: Path, localName: Name) {} + +object FQName { + val fQName = (packagePath: Path) => + (modulePath: Path) => + (localName: Name) => FQName(packagePath, modulePath, localName) +} diff --git a/core/src/com/morganstanley/morphir/ir/Name.scala b/core/src/com/morganstanley/morphir/ir/Name.scala index 2e2598cf..4f66fd41 100644 --- a/core/src/com/morganstanley/morphir/ir/Name.scala +++ b/core/src/com/morganstanley/morphir/ir/Name.scala @@ -7,6 +7,9 @@ import scala.annotation.tailrec case class Name private[ir] (value: List[String]) extends AnyVal object Name { + def apply(firstWord: String, otherWords: String*): Name = + Name(firstWord :: otherWords.toList) + def fromString(str: String): Name = { val pattern = """[a-zA-Z][a-z]*|[0-9]+""".r Name(pattern.findAllIn(str).toList.map(_.toLowerCase())) diff --git a/core/src/com/morganstanley/morphir/ir/Path.scala b/core/src/com/morganstanley/morphir/ir/Path.scala new file mode 100644 index 00000000..4fb38539 --- /dev/null +++ b/core/src/com/morganstanley/morphir/ir/Path.scala @@ -0,0 +1,41 @@ +package com.morganstanley.morphir.ir + +case class Path(value: List[Name]) extends AnyVal { + @inline def toList: List[Name] = value + def mapSegments[A](fn: Name => A): List[A] = + value.map(fn) +} + +object Path { + + val empty: Path = Path(List.empty) + + def apply(head: Name, rest: Name*): Path = + Path(head :: rest.toList) + + def fromString(str: String): Path = { + val separatorRegex = """[^\w\s]+""".r + fromList(separatorRegex.split(str).map(Name.fromString).toList) + } + + def toString(nameToString: Name => String, sep: String, path: Path): String = + path.mapSegments(nameToString).mkString(sep) + + def fromList(names: List[Name]): Path = + Path(names) + + def toList(path: Path): List[Name] = path.value + + def isPrefixOf(prefix: Path, path: Path): Boolean = (prefix, path) match { + // empty path is a prefix of any other path + case (Path.empty, _) => true + // empty path has no prefixes except the empty prefix captured above + case (_, Path.empty) => false + case (Path(prefixHead :: prefixTail), Path(pathHead :: pathTail)) => + if (prefixHead == pathHead) + isPrefixOf(Path(prefixTail), Path(pathTail)) + else + false + } + +} diff --git a/core/src/com/morganstanley/morphir/ir/QName.scala b/core/src/com/morganstanley/morphir/ir/QName.scala new file mode 100644 index 00000000..e664d538 --- /dev/null +++ b/core/src/com/morganstanley/morphir/ir/QName.scala @@ -0,0 +1,22 @@ +package com.morganstanley.morphir.ir + +case class QName(modulePath: Path, localName: Name) { + def toTuple: (Path, Name) = modulePath -> localName +} + +object QName { + @inline def toTuple(qname: QName): (Path, Name) = qname.toTuple + def fromTuple(path: Path, name: Name): QName = QName(path, name) + @inline def getModulePath(qname: QName): Path = qname.modulePath + @inline def getLocalName(qname: QName): Name = qname.localName + + def toString( + pathPartToString: Name => String, + nameToString: Name => String, + sep: String, + qname: QName + ) = + (qname.modulePath.toList.map(pathPartToString) ++ nameToString( + qname.localName + )).mkString(sep) +} diff --git a/core/src/com/morganstanley/morphir/ir/advanced/Type.scala b/core/src/com/morganstanley/morphir/ir/advanced/Type.scala index 35c47cbb..f197c2cb 100644 --- a/core/src/com/morganstanley/morphir/ir/advanced/Type.scala +++ b/core/src/com/morganstanley/morphir/ir/advanced/Type.scala @@ -1,10 +1,21 @@ package com.morganstanley.morphir.ir.advanced -import com.morganstanley.morphir.ir.Name +import com.morganstanley.morphir.ir.{FQName, Name} sealed trait Type[+Extra] object Type { case class Variable[A](name: Name, extra: A) extends Type[A] + case class Reference[A]( + typeName: FQName, + typeParameters: List[Type[A]], + extra: A + ) extends Type[A] + + case class Tuple[A](elementTypes: List[Type[A]], extra: A) extends Type[A] + case class Record[A](fieldTypes: List[Field[A]], extra: A) extends Type[A] + case class ExtensibleRecord[A]() extends Type[A] case class Function[A](argType: Type[A], returnType: Type[A], extra: A) extends Type[A] case class Unit[A](extra: A) extends Type[A] + + case class Field[A](name: Name, fieldType: Type[A]) } diff --git a/core/test/src/com/morganstanley/morphir/ir/PathSpec.scala b/core/test/src/com/morganstanley/morphir/ir/PathSpec.scala new file mode 100644 index 00000000..da84559a --- /dev/null +++ b/core/test/src/com/morganstanley/morphir/ir/PathSpec.scala @@ -0,0 +1,82 @@ +package com.morganstanley.morphir.ir + +import zio.test._ +import zio.test.Assertion._ +import zio.test.environment._ +import scala.language.implicitConversions + +object PathSpec extends DefaultRunnableSpec { + + def spec = suite("PathSpec")( + suite("Creating a Path from a String")( + test("Should be possible when given a simple string") { + assert(Path.fromString("Person"))( + equalTo(Path(List(Name.fromString("person")))) + ) + }, + test("Should be possible when given a dotted string") { + assert(Path.fromString("blog.Author"))( + equalTo( + Path( + List(Name.fromList(List("blog")), Name.fromList(List("author"))) + ) + ) + ) + } + ), + suite("Transforming a Path into a String")( + test("Should be supported (a)") { + val input = + Path( + Name("foo", "bar"), + Name("baz") + ) + + assert(Path.toString(Name.toTitleCase, ".", input))( + equalTo("FooBar.Baz") + ) + }, + test("Should be supported (b)") { + val input = + Path( + Name("foo", "bar"), + Name("baz") + ) + + assert(Path.toString(Name.toSnakeCase, "/", input))( + equalTo("foo_bar/baz") + ) + } + ), + suite("Transforming to a list of Names")( + test("Should be support via the toList function") { + assert( + Path.toList(Path(Name("Com", "Example"), Name("Hello", "World"))) + )( + equalTo(List(Name("Com", "Example"), Name("Hello", "World"))) + ) + } + ), + 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") + + assert(Path.isPrefixOf(prefix = prefix, path = sut))(isTrue) + }, + test("""Return false: Given path is "foo/foo" and prefix is "bar" """) { + val sut = Path.fromString("foo/foo") + val prefix = Path.fromString("bar") + + assert(Path.isPrefixOf(prefix = prefix, path = sut))(isFalse) + }, + test("""Return true: Given equal paths""") { + val sut = Path.fromString("foo/bar/baz") + val prefix = sut + + assert(Path.isPrefixOf(prefix = prefix, path = sut))(isTrue) + } + ) + ) + +} diff --git a/core/test/src/com/morganstanley/morphir/ir/QNameSpec.scala b/core/test/src/com/morganstanley/morphir/ir/QNameSpec.scala new file mode 100644 index 00000000..74194c58 --- /dev/null +++ b/core/test/src/com/morganstanley/morphir/ir/QNameSpec.scala @@ -0,0 +1,12 @@ +package com.morganstanley.morphir.ir + +import zio.test._ +import zio.test.Assertion._ +import zio.test.environment._ +import scala.language.implicitConversions + +object QNameSpec extends DefaultRunnableSpec { + + def spec = suite("QNameSpec")() + +}