Skip to content

Commit

Permalink
Merge pull request #300 from michelchan/start-ir
Browse files Browse the repository at this point in the history
Starting to add IR
  • Loading branch information
michelchan authored Oct 3, 2024
2 parents c4f2747 + f5dde9a commit 3f185d4
Show file tree
Hide file tree
Showing 11 changed files with 1,193 additions and 0 deletions.
61 changes: 61 additions & 0 deletions morphir/src/org/finos/morphir/ir/gen1/moduleName.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package org.finos.morphir.ir.gen1

trait ModuleNameModule { self: NameModule with PathModule =>

/** A module name is a unique identifier for a module within a package. It is represented by a `Path`, which is a
* "list" of names.
*/
sealed case class ModuleName(path: Path) { self =>

/// Construct a new module name by concatting the given module name to this one.
def ++(other: ModuleName): ModuleName = ModuleName(path ++ other.path)

/// Construct a new module name by concatting the given module path to this one.
def ++(other: Path): ModuleName = ModuleName(path ++ other)

/// Construct a new module name by concatting the given local name to this module name.
def /(name: Name): ModuleName = ModuleName(path / name)
/// Construct a new module name by concatting the given local name to this module name.
def /(name: String): ModuleName = ModuleName(path / Name(name))

// Need a non-symbolic version of this for QualifiedModuleName, strange compilation errors
// happen in QualifiedModuleName./(str: String) implementation when using `moduleName / str` otherwise
def addPart(name: String): ModuleName = ModuleName(path / Name(name))

/// Check if the module name is empty.
@inline def isEmpty: Boolean = path.isEmpty

/// Get the name of this module.
/// For example if the module name is `Morphir.SDK.Basics` then the name is `Basics`.
def name: Name =
self match {
case ModuleName(Path(Vector())) => Name.empty
case ModuleName(Path(segments)) => segments.last
}

// Get the name of this module if a name is present.
def nameOption: Option[Name] =
self match {
case ModuleName(Path(Vector())) => None
case ModuleName(Path(segments)) => Some(segments.last)
}

/// Convert this module name to a `Path`.
@inline def toPath: Path = path
override def toString: String = path.toString
}

object ModuleName {
/// Create an empty module name.
val empty: ModuleName = ModuleName(Path.empty)

def apply(input: String): ModuleName = fromString(input)

def fromString(input: String): ModuleName = ModuleName(Path.fromString(input))

def apply(parts: Name*): ModuleName = ModuleName(Path.fromIterable(parts))
def fromIterable(segments: Iterable[Name]): ModuleName = ModuleName(Path.fromIterable(segments))
def fromStrings(parts: String*): ModuleName = ModuleName(Path.fromStrings(parts: _*))
}

}
166 changes: 166 additions & 0 deletions morphir/src/org/finos/morphir/ir/gen1/name.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package org.finos.morphir.ir.gen1

import scala.annotation.tailrec

trait NameModule {

/** `Name` is an abstraction of human-readable identifiers made up of words. This abstraction allows us to use the
* same identifiers across various naming conventions used by the different frontend and backend languages Morphir
* integrates with.
*/
sealed case class Name private (toList: List[String]) {
self =>
// def :+(that: String): Name = Name(self.toList :+ that)

// def +:(that: String): Name = Name(that +: self.toList)

def ++(that: Name): Name = Name(self.toList ++ that.toList)

def +(that: String): Name = self ++ Name.fromString(that)

// def /(that: Name): Path = Path(IndexedSeq(self, that))

def humanize: List[String] = {
val words = toList
val join: List[String] => String = abbrev => abbrev.map(_.toUpperCase()).mkString("")

@tailrec
def loop(
prefix: List[String],
abbrev: List[String],
suffix: List[String]
): List[String] =
suffix match {
case Nil =>
abbrev match {
case Nil => prefix
case _ => prefix ++ List(join(abbrev))
}
case first :: rest =>
if (first.length() == 1)
loop(prefix, abbrev ++ List(first), rest)
else
abbrev match {
case Nil => loop(prefix ++ List(first), List.empty, rest)
case _ =>
loop(prefix ++ List(join(abbrev), first), List.empty, rest)
}
}

loop(List.empty, List.empty, words.toList)
}

/** Maps segments of the `Name`.
*/
def mapParts(f: String => String): Name = Name(self.toList.map(f))

def mkString(f: String => String)(sep: String): String =
toList.map(f).mkString(sep)

def render(implicit renderer: NameRenderer): String = renderer.render(self)

def toUpperCase: String = mkString(part => part.toUpperCase)("")

// def toLocalName(implicit renderer: Name.Renderer): LocalName = {
// val localNameStr = render
// LocalName(localNameStr)
// }

def toLowerCase: String =
mkString(part => part.toLowerCase)("")

def toCamelCase: String =
toList match {
case Nil => ""
case head :: tail =>
(head :: tail.map(_.capitalize)).mkString("")
}

def toKebabCase: String =
humanize.mkString("-")

def toSnakeCase: String =
humanize.mkString("_")

def toTitleCase: String =
toList
.map(_.capitalize)
.mkString("")

override def toString: String = toList.mkString("[", ",", "]")
}

object Name {

val empty: Name = Name(Nil)

private[morphir] def wrap(value: List[String]): Name = Name(value)

private[morphir] def wrap(value: Array[String]): Name = Name(value.toList)

def apply(first: String, rest: String*): Name =
fromIterable(first +: rest)

private val pattern = """([a-zA-Z][a-z]*|[0-9]+)""".r

/** Converts a list of strings into a name. NOTE: When this function is used, the strings are used as is to
* construct the name, and don't go through any processing. This behavior is desired here as it is consistent with
* Morphir's semantics for this function as defined in the `morphir-elm` project.
*/
def fromList(list: List[String]): Name = Name(list)

/** Converts a list of strings into a name. NOTE: When this function is used, the strings are used as is to
* construct the name, and don't go through any processing. This behavior is desired here as it is consistent with
* Morphir's semantics for this function as defined in the `morphir-elm` project.
*/
def fromList(list: String*): Name = fromList(list.toList)

def fromIterable(iterable: Iterable[String]): Name =
wrap(iterable.flatMap(str => pattern.findAllIn(str)).map(_.toLowerCase).toList)

def fromString(str: String): Name =
Name(pattern.findAllIn(str).toList.map(_.toLowerCase()))

def toList(name: Name): List[String] = name.toList

@inline def toTitleCase(name: Name): String = name.toTitleCase

@inline def toCamelCase(name: Name): String = name.toCamelCase

@inline def toSnakeCase(name: Name): String = name.toSnakeCase

@inline def toKebabCase(name: Name): String = name.toKebabCase

@inline def toHumanWords(name: Name): List[String] = name.humanize

object VariableName {
def unapply(name: Name): Option[String] =
Some(name.toCamelCase)
}

}

trait NameRenderer extends (Name => String) {
final def render(name: Name): String = apply(name)
}

object NameRenderer {
object CamelCase extends NameRenderer {
def apply(name: Name): String = name.toCamelCase
}

object KebabCase extends NameRenderer {
def apply(name: Name): String = name.toKebabCase
}

object SnakeCase extends NameRenderer {
def apply(name: Name): String = name.toSnakeCase
}

object TitleCase extends NameRenderer {
def apply(name: Name): String = name.toTitleCase
}

implicit val default: NameRenderer = TitleCase
}
}
48 changes: 48 additions & 0 deletions morphir/src/org/finos/morphir/ir/gen1/namespace.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.finos.morphir.ir.gen1

trait NamespaceModule { self: NameModule with PathModule with ModuleNameModule =>
sealed case class Namespace(path: Path) { self =>
def ++(name: Namespace): Namespace = Namespace(path ++ path)
def /(segment: String): Namespace = Namespace(path ++ Path.fromString(segment))
def /(names: String*): Namespace = Namespace(path ++ Path.fromIterable(names.map(Name.fromString(_))))
def /(names: Iterable[String]): Namespace = Namespace(path ++ Path.fromIterable(names.map(Name.fromString(_))))

@inline def toPath: Path = path
def parts(implicit renderer: NamespaceRenderer): IndexedSeq[String] = path.parts(renderer)

def render(implicit renderer: NamespaceRenderer): String = renderer(path)
/// An alias for `render`
def show(implicit renderer: NamespaceRenderer): String = render
def toModuleName: ModuleName = ModuleName(path)

override def toString(): String = render
}

object Namespace {
val ns: Namespace = Namespace(Path.empty)

def apply(parts: Name*): Namespace = Namespace(Path.fromIterable(parts))
def fromIterable(segments: Iterable[Name]): Namespace = Namespace(Path.fromIterable(segments))
def fromModuleName(moduleName: ModuleName): Namespace = Namespace(moduleName.path)
def fromStrings(parts: String*): Namespace = Namespace(Path.fromStrings(parts: _*))

def fromPath(path: Path): Namespace = Namespace(path)

}

sealed case class NamespaceRenderer(separator: String, nameRenderer: NameRenderer) extends (Path => String) {
def apply(path: Path): String = path.toString(nameRenderer, separator)
final def render(path: Path): String = apply(path)
}

object NamespaceRenderer {
val CamelCase: NamespaceRenderer = NamespaceRenderer(".", NameRenderer.CamelCase)
val KebabCase: NamespaceRenderer = NamespaceRenderer(".", NameRenderer.KebabCase)
val SnakeCase: NamespaceRenderer = NamespaceRenderer(".", NameRenderer.SnakeCase)
val TitleCase: NamespaceRenderer = NamespaceRenderer(".", NameRenderer.TitleCase)

implicit val default: NamespaceRenderer = TitleCase
implicit def toPathRenderer(renderer: NamespaceRenderer): PathRenderer =
PathRenderer(renderer.separator, renderer.nameRenderer)
}
}
120 changes: 120 additions & 0 deletions morphir/src/org/finos/morphir/ir/gen1/path.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package org.finos.morphir.ir.gen1

import scala.annotation.tailrec

trait PathModule { self: NameModule =>

sealed case class Path(segments: Vector[Name]) {
self =>

def ++(that: Path): Path = Path(segments ++ that.segments)
// def ::(name: Name): QName = QName(self.toPath, name)

/** Indicates whether this path is empty. */
def isEmpty: Boolean = toList.isEmpty

def toList: List[Name] = segments.toList

/** Constructs a new path by combining this path with the given name. */
def /(name: Name): Path = Path(segments ++ List(name))

/** Constructs a new path by combining this path with the given path. */
def /(that: Path): Path = Path(segments ++ that.toList)
// def %(other: Path): PackageAndModulePath =
// PackageAndModulePath(PackageName(self), ModulePath(other))

def zip(other: Path): (Path, Path) = (self, other)

def toString(f: Name => String, separator: String): String =
toList.map(f).mkString(separator)

/** Checks if this path is a prefix of provided path */
def isPrefixOf(path: Path): Boolean = Path.isPrefixOf(self, path)

def parts(implicit renderer: PathRenderer): IndexedSeq[String] = segments.map(_.render(renderer.nameRenderer))

def render(implicit renderer: PathRenderer): String = renderer(self)
def render(separator: String)(implicit nameRenderer: NameRenderer): String =
render(PathRenderer(separator, nameRenderer))

// def toPackageName(implicit renderer: Name.Renderer = Name.Renderer.TitleCase): PackageName = {
// val nsSegments = PackageName.segments(segments.map(_.render))
// PackageName.fromIterable(nsSegments)
// }

// def toNamespace(implicit renderer: Name.Renderer = Name.Renderer.TitleCase): Namespace = {
// val nsSegments = Namespace.segments(segments.map(_.render))
// Namespace.fromIterable(nsSegments)
// }

override def toString(): String = render
}

object Path {
val separatorRegex = """[^\w\s]+""".r
val empty: Path = Path(Vector.empty)
val root: Path = Path(Vector.empty)

def apply(first: String, rest: String*): Path =
if (rest.isEmpty) Path(Vector(Name.fromString(first)))
else Path.fromIterable(Name.fromString(first) +: rest.map(Name.fromString(_)))

def apply(first: Name, rest: Name*): Path =
if (rest.isEmpty) Path(Vector(first))
else Path(first +: rest.toVector)

/** Translates a string into a path by splitting it into names along special characters. The algorithm will treat
* any non-word characters that are not spaces as a path separator.
*/
def fromString(str: String): Path =
fromArray(separatorRegex.split(str).map(Name.fromString))

def toString(f: Name => String, separator: String, path: Path): String =
path.toString(f, separator)

/// Converts an array of names into a path.
@inline def fromArray(names: Array[Name]): Path = Path(names.toVector)
/// Converts names into a path.
@inline def fromIterable(names: Iterable[Name]): Path = Path(names.toVector)
/// Converts a list of names into a path.
@inline def fromList(names: List[Name]): Path = Path(names.toVector)
/// Converts a list of names into a path.
@inline def fromList(names: Name*): Path = Path(names.toVector)

def fromStrings(names: String*): Path = Path(names.map(Name.fromString).toVector)
/// Converts names into a path.
@inline def fromVector(names: Vector[Name]): Path = Path(names)

@inline def toList(path: Path): List[Name] = path.toList.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
}

private[morphir] def unsafeMake(parts: Name*): Path = Path(parts.toVector)
}

sealed case class PathRenderer(separator: String, nameRenderer: NameRenderer) extends (Path => String) {
def apply(path: Path): String = path.toString(nameRenderer, separator)
final def render(path: Path): String = apply(path)
}

object PathRenderer {
val CamelCase: PathRenderer = PathRenderer(".", NameRenderer.CamelCase)
val KebabCase: PathRenderer = PathRenderer(".", NameRenderer.KebabCase)
val SnakeCase: PathRenderer = PathRenderer(".", NameRenderer.SnakeCase)
val TitleCase: PathRenderer = PathRenderer(".", NameRenderer.TitleCase)

implicit val default: PathRenderer = TitleCase
}
}
Loading

0 comments on commit 3f185d4

Please sign in to comment.