Skip to content

Commit

Permalink
Improvements from library users
Browse files Browse the repository at this point in the history
* Parse attachments correctly and unambiguously
* Make all readability words not have cuts in them
* Add the various "With" classes to Nebula to allow
  users to have easier access to Nebula contents
* Remove the TokenParserTest println
* Add testing for parsing attachments.
  • Loading branch information
reidspencer committed Feb 20, 2025
1 parent b0cbc5e commit 2d1334f
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 50 deletions.
1 change: 1 addition & 0 deletions language/input/full/context.riddl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ context other is {
???
} with {
by author Reid
attachment foo is text/plain as "nada"
}
context context is {
adaptor adaptor from context other is { ??? }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
package com.ossuminc.riddl.language

import com.ossuminc.riddl.language.AST.Contents.unapply
import com.ossuminc.riddl.language.AST.{WithContexts, WithDomains, WithModules}
import com.ossuminc.riddl.utils.{Await, PlatformContext, URL}
import com.ossuminc.riddl.language.Messages.Messages
import com.ossuminc.riddl.language.parsing.{Keyword, RiddlParserInput}

import scala.collection.{mutable, immutable}
import scala.collection.{immutable, mutable}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.reflect.{ClassTag, classTag}
import scala.annotation.{tailrec, targetName, unused}
Expand Down Expand Up @@ -369,14 +370,15 @@ object AST:
override def format: String = url.toExternalForm
end URLDescription

sealed trait Attachment extends Meta with WithIdentifier

/** */
case class FileAttachment(
loc: At,
id: Identifier,
mimeType: String,
inFile: LiteralString
) extends Meta
with WithIdentifier:
) extends Attachment:
def format: String = identify
end FileAttachment

Expand All @@ -386,16 +388,14 @@ object AST:
id: Identifier,
mimeType: String,
value: LiteralString
) extends Meta
with WithIdentifier:
) extends Attachment:
def format: String = identify
end StringAttachment

case class ULIDAttachment(
loc: At,
ulid: ULID
) extends Meta
with WithIdentifier:
) extends Attachment:
override def id: Identifier = Identifier(At.empty, "ULID")
def format: String = identify
end ULIDAttachment
Expand Down Expand Up @@ -532,6 +532,10 @@ object AST:
/** A lazily constructed mutable [[Seq]] of [[AuthorRef]] */
def terms: Seq[Term] = metadata.filter[Term]

def stringAttachments: Seq[StringAttachment] = metadata.filter[StringAttachment]

def fileAttachments: Seq[FileAttachment] = metadata.filter[FileAttachment]

/** Return the [[OptionValue]]s in the meta data */
def options: Seq[OptionValue] = metadata.filter[OptionValue]

Expand All @@ -541,9 +545,10 @@ object AST:
/** Get the value of `name`'d option, if there is one. */
def getOptionValue(name: String): Option[OptionValue] = options.find(_.name == name)

/** Get the ULID associated with the definition. There can only be one and they are
* assigned when this method is called, spreading their definition across the access
* patterns, on purpose. */
/** Get the ULID associated with the definition. There can only be one and they are assigned
* when this method is called, spreading their definition across the access patterns, on
* purpose.
*/
lazy val ulid: ULID =
metadata.find("ULID") match
case Some(ulid: ULIDAttachment) => ulid.ulid
Expand Down Expand Up @@ -1096,7 +1101,25 @@ object AST:
case class Nebula(
loc: At,
contents: Contents[NebulaContents] = Contents.empty[NebulaContents]()
) extends Branch[NebulaContents]:
) extends Branch[NebulaContents]
with WithAdaptors[NebulaContents]
with WithAuthors[NebulaContents]
with WithComments[NebulaContents]
with WithConstants[NebulaContents]
with WithContexts[NebulaContents]
with WithDomains[NebulaContents]
with WithEntities[NebulaContents]
with WithEpics[NebulaContents]
with WithFunctions[NebulaContents]
with WithInvariants[NebulaContents]
with WithModules[NebulaContents]
with WithProjectors[NebulaContents]
with WithRepositories[NebulaContents]
with WithSagas[NebulaContents]
with WithStreamlets[NebulaContents]
with WithTypes[NebulaContents]
with WithUsers[NebulaContents]:

override def isRootContainer: Boolean = false

override def id: Identifier = Identifier(loc, "Nebula")
Expand Down Expand Up @@ -1168,7 +1191,8 @@ object AST:
loc: At,
id: Identifier,
definition: Seq[LiteralString]
) extends Meta with WithIdentifier :
) extends Meta
with WithIdentifier:
def format: String = s"${Keyword.term} ${id.format}"
end Term

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import fastparse.*
import fastparse.MultiLineWhitespace.*
import wvlet.airframe.ulid.ULID

import java.lang.Character.isLowerCase
import java.net.URI
import java.nio.file.Files
import scala.reflect.{ClassTag, classTag}
Expand Down Expand Up @@ -176,27 +177,27 @@ private[parsing] trait CommonParser(using pc: PlatformContext)
}
}

private def mimeTypeChars(in: Char): Boolean =
isLowerCase(in) | in == '.' || in == '-' || in == '*'
end mimeTypeChars

def mimeType[u: P]: P[String] = {
P(
("application" | "audio" | "example" | "font" |
"image" | "model" | "text" | "video") ~~ "/" ~~
CharIn("a-z", ".*", "\\-").rep(1)
CharsWhile(mimeTypeChars)
).!
}

private def fileAttachment[u: P]: P[FileAttachment] = {
private def attachment[u: P]: P[Attachment] =
P(
Index ~ Keywords.attachment ~ identifier ~ is ~ mimeType ~ in ~ Keywords.file ~ literalString ~ Index
).map { case (off1, id, mimeType, fileName, off2) =>
FileAttachment(at(off1, off2), id, mimeType, fileName)
}
}

private def stringAttachment[u: P]: P[StringAttachment] =
P(
Index ~ Keywords.attachment ~ identifier ~ is ~ mimeType ~ as ~ literalString ~ Index
).map { case (off1, id, mimeType, value, off2) =>
StringAttachment(at(off1, off2), id, mimeType, value)
Index ~ Keywords.attachment ~ identifier ~ is ~ mimeType ~
((in.! ~ Keywords.file ~ literalString) | (as.! ~ literalString)) ~ Index
).map {
case (off1, id, mimeType, ("in", fileName), off2) =>
FileAttachment(at(off1, off2), id, mimeType, fileName)
case (off1, id, mimeType, ("as", value), off2) =>
StringAttachment(at(off1, off2), id, mimeType, value)
}

private def ulidAttachment[u: P]: P[ULIDAttachment] =
Expand Down Expand Up @@ -224,10 +225,9 @@ private[parsing] trait CommonParser(using pc: PlatformContext)

private def metaData[u: P]: P[MetaData] =
P(
briefDescription | description | term | option | authorRef | fileAttachment |
stringAttachment | ulidAttachment | comment
)
.asInstanceOf[P[MetaData]]
briefDescription | description | term | option | authorRef | attachment |
ulidAttachment | comment
).asInstanceOf[P[MetaData]]

def withMetaData[u: P]: P[Seq[MetaData]] = {
P(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,37 @@ import com.ossuminc.riddl.language.parsing.Keywords.keyword

trait Readability {

def and[u: P]: P[Unit] = keyword("and")
def readable[u: P](key: String): P[Unit] = {
P(key)
}

def and[u: P]: P[Unit] = readable("and")

def are[u: P]: P[Unit] = keyword("are")
def are[u: P]: P[Unit] = readable("are")

def as[u: P]: P[Unit] = keyword("as")
def as[u: P]: P[Unit] = readable("as")

def at[u: P]: P[Unit] = keyword("at")
def at[u: P]: P[Unit] = readable("at")

def by[u: P]: P[Unit] = keyword("by")
def by[u: P]: P[Unit] = readable("by")

def `for`[u: P]: P[Unit] = keyword("for")
def `for`[u: P]: P[Unit] = readable("for")

def from[u: P]: P[Unit] = keyword("from")
def from[u: P]: P[Unit] = readable("from")

def in[u: P]: P[Unit] = keyword("in")
def in[u: P]: P[Unit] = readable("in")

def of[u: P]: P[Unit] = keyword("of")
def of[u: P]: P[Unit] = readable("of")

def so[u: P]: P[Unit] = keyword("so")
def so[u: P]: P[Unit] = readable("so")

def that[u: P]: P[Unit] = keyword("that")
def that[u: P]: P[Unit] = readable("that")

def to[u: P]: P[Unit] = keyword("to")
def to[u: P]: P[Unit] = readable("to")

def wants[u: P]: P[Unit] = keyword("wants")
def wants[u: P]: P[Unit] = readable("wants")

def `with`[u: P]: P[Unit] = keyword("with")
def `with`[u: P]: P[Unit] = readable("with")

def anyReadability[u: P]: P[Unit] = {
P(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ package com.ossuminc.riddl.language.parsing

import com.ossuminc.riddl.language.AST.*
import com.ossuminc.riddl.language.At
import com.ossuminc.riddl.utils.{PlatformContext,URL}
import com.ossuminc.riddl.utils.{PlatformContext, URL}
import org.scalatest.matchers.must.Matchers.must
import org.scalatest.{TestData, fixture}
import sourcecode.Text.generate

import scala.concurrent.ExecutionContext.Implicits.global

Expand Down Expand Up @@ -129,11 +131,32 @@ abstract class CommonParserTest(using PlatformContext) extends AbstractParsingTe
actual mustBe LiteralString((1, 1, rpi), input.data.drop(1).dropRight(1))
}
}
"attachments parse correctly" in { (td: TestData) =>
val input = RiddlParserInput(
"""context foo {
| ???
|} with {
| attachment infile is text/plain in file "nada.txt"
| attachment inline is text/plain as "nada"
|}""".stripMargin,
td
)
parseDefinition[Context](input) match {
case Left(errors) =>
val msg = errors.map(_.format).mkString
fail(msg)
case Right((context: Context, _)) =>
context.stringAttachments.size must be(1)
context.stringAttachments.head.value.s must be("nada")
context.fileAttachments.size must be(1)
context.fileAttachments.head.inFile.s must be("nada.txt")
}
}
}
"NoWhiteSpaceParsers" should {
"handle a URL" in { (td: TestData) =>
val input = RiddlParserInput("https://www.wordnik.com/words/phi",td)
parse[URL,URL](input, StringParser("").httpUrl(_), identity) match {
val input = RiddlParserInput("https://www.wordnik.com/words/phi", td)
parse[URL, URL](input, StringParser("").httpUrl(_), identity) match {
case Left(errors) =>
fail(errors.format)
case Right((actual, _)) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -781,17 +781,16 @@ abstract class TokenParserTest(using pc: PlatformContext) extends AbstractParsin
val result: Either[Messages, List[String]] =
pc.withOptions(pc.options.copy(showTimes = true)) { _ =>
Timer.time("parseToTokensAndText") {
TopLevelParser.mapTextAndToken[String](rpi)( (x,y) => mapTokens(x,y))
TopLevelParser.mapTextAndToken[String](rpi)((x, y) => mapTokens(x, y))
}
}
result match
case Left(messages) =>
fail(messages.format)
case Right(list) =>
list.length must be(404)
list.head mustBe("Keyword(context)")
list(1) mustBe("Identifier(full)")
print(list.mkString("\n"))
list.head mustBe ("Keyword(context)")
list(1) mustBe ("Identifier(full)")
end match
}
}

0 comments on commit 2d1334f

Please sign in to comment.