Skip to content

Commit

Permalink
feat(didparser): Adding amtlr4 grammar did parser with specification …
Browse files Browse the repository at this point in the history
…and tests (#10)

* Add dependencies to manage antlr grammar in multiplatform

* Generate antlr4 grammar parsers and lexers with gradle task

* Remove intermediate interp and tokens amtlr grammar files

* Remove auto generated files but keep tests working.

* few changes to fix Gradle

- Gradle task should not be a dependency on all tasks

* add another exclude

Co-authored-by: Ahmed Moussa <[email protected]>
  • Loading branch information
elribonazo and hamada147 authored Jan 21, 2023
1 parent 1e4faf8 commit 0d7dac1
Show file tree
Hide file tree
Showing 9 changed files with 340 additions and 2 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ buildscript {
classpath("com.android.tools.build:gradle:7.2.2")
classpath("com.google.protobuf:protobuf-gradle-plugin:0.9.1")
classpath("com.squareup.sqldelight:gradle-plugin:1.5.4")
classpath("com.github.piacenti:antlr-kotlin-gradle-plugin:0.0.14")
}
}

Expand Down
71 changes: 69 additions & 2 deletions castor/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,32 @@ kotlin {
}

sourceSets {
val commonAntlr by creating {
kotlin.srcDir("build/generated-src/commonAntlr/kotlin")
dependencies {
api(kotlin("stdlib-common"))
api("com.github.piacenti:antlr-kotlin-runtime:0.0.14")
}
}
val commonMain by getting {
dependsOn(commonAntlr)
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation(project(":domain"))
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
val jvmMain by getting
val jvmMain by getting {
dependencies {
api(kotlin("stdlib-jdk8"))
api(kotlin("reflect"))
}
}
val jvmTest by getting {
dependencies {
implementation("junit:junit:4.13.2")
Expand All @@ -91,7 +105,15 @@ kotlin {
implementation("junit:junit:4.13.2")
}
}
val jsMain by getting
val jsMain by getting {
dependsOn(commonAntlr)
dependencies {
implementation("com.github.piacenti:antlr-kotlin-runtime-js:0.0.14")
implementation("org.jetbrains.kotlin:kotlin-stdlib-common:1.7.20")
implementation("org.jetbrains.kotlin:kotlin-stdlib-js:1.7.20")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
}
}
val jsTest by getting

all {
Expand Down Expand Up @@ -126,6 +148,20 @@ android {
}
}

ktlint {
filter {
exclude("build/generated-src/**")
exclude("**/generated/**")
exclude("**/generated-src/**")
exclude {
it.file.path.contains("generated-src")
}
exclude {
it.file.path.contains("generated")
}
}
}

// Dokka implementation
tasks.withType<DokkaTask> {
moduleName.set(project.name)
Expand All @@ -150,3 +186,34 @@ tasks.withType<DokkaTask> {
// }
// }
// }

val antlrGenerationTask by tasks.register<com.strumenta.antlrkotlin.gradleplugin.AntlrKotlinTask>("generateKotlinCommonGrammarSource") {
// the classpath used to run antlr code generation
antlrClasspath = configurations.detachedConfiguration(
project.dependencies.create("com.github.piacenti:antlr-kotlin-runtime:0.0.14")
)
maxHeapSize = "64m"
packageName = "io.iohk.atala.prism.castor.antlrgrammar"
arguments = listOf("-long-messages", "-Dlanguage=JavaScript")
source = project.objects
.sourceDirectorySet("antlr", "antlr")
.srcDir("src/commonAntlr/antlr").apply {
include("*.g4")
}
// outputDirectory is required, put it into the build directory
// if you do not want to add the generated sources to version control
outputDirectory = File("build/generated-src/commonAntlr/kotlin")
// use this setting if you want to add the generated sources to version control
// outputDirectory = File("src/commonAntlr/kotlin")
}

tasks.matching {
it.name == "compileCommonAntlrKotlinMetadata" ||
// it.name == "compileCommonMainKotlinMetadata" ||
it.name == "compileReleaseKotlinAndroid" ||
it.name == "compileDebugKotlinAndroid" ||
it.name == "compileKotlinJs" ||
it.name == "compileKotlinJvm"
}.all {
this.dependsOn(antlrGenerationTask)
}
35 changes: 35 additions & 0 deletions castor/src/commonAntlr/antlr/DIDAbnf.g4
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
grammar DIDAbnf;

did
: SCHEMA ':' method_name ':' method_specific_id EOF
;

method_name
: (ALPHA | DIGIT)*
;

method_specific_id
: idchar* ( ':' idchar+ )?
;

idchar
: ( ALPHA | DIGIT | PERIOD | DASH | UNDERSCORE | PCT_ENCODED )
;

fragment D : ('d' | 'D');
fragment I : ('i' | 'I');
SCHEMA : D I D;

fragment LOWERCASE : [a-z];
fragment UPPERCASE : [A-Z];
ALPHA : ( LOWERCASE | UPPERCASE );

fragment HEX : [0-9a-fA-F];
DIGIT : [0-9];
PCT_ENCODED : PERCENT HEX HEX;
PERCENT : '%';
DASH : '-';
PERIOD : '.';
COLON : ':';
UNDERSCORE : '_';

61 changes: 61 additions & 0 deletions castor/src/commonAntlr/antlr/DIDUrlAbnf.g4
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
grammar DIDUrlAbnf;

did_url
: did path? query? frag? EOF
;

did
: SCHEMA ':' method_name ':' method_specific_id
;

method_name
: string
;

method_specific_id
: ( string ':'? )* string
;

path
: ('/' string)* '/'?
;

query
: '?' search
;

frag
: '#' (string | DIGIT)
;

search
: searchparameter ('&' searchparameter)*
;

searchparameter
: string ('=' (string | DIGIT | HEX))?
;

string
: STRING
| DIGIT
;

fragment D : ('d' | 'D');
fragment I : ('i' | 'I');
SCHEMA : D I D;

fragment LOWERCASE : [a-z];
fragment UPPERCASE : [A-Z];
ALPHA : ( LOWERCASE | UPPERCASE );

DIGIT : [0-9];
PCT_ENCODED : PERCENT HEX HEX;
PERCENT : '%';
DASH : '-';
PERIOD : '.';
COLON : ':';
UNDERSCORE : '_';
HEX : ('%' [a-fA-F0-9] [a-fA-F0-9])+;

STRING: ([a-zA-Z~0-9] | HEX) ([a-zA-Z0-9.+-] | HEX)*;
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.iohk.atala.prism.castor

import io.iohk.atala.prism.castor.antlrgrammar.DIDAbnfLexer
import io.iohk.atala.prism.castor.antlrgrammar.DIDAbnfParser
import io.iohk.atala.prism.domain.models.DID
import org.antlr.v4.kotlinruntime.CharStreams
import org.antlr.v4.kotlinruntime.CommonTokenStream
import org.antlr.v4.kotlinruntime.DefaultErrorStrategy
import org.antlr.v4.kotlinruntime.Parser
import org.antlr.v4.kotlinruntime.ParserRuleContext
import org.antlr.v4.kotlinruntime.RecognitionException
import org.antlr.v4.kotlinruntime.Token
import org.antlr.v4.kotlinruntime.tree.ParseTree
import org.antlr.v4.kotlinruntime.tree.ParseTreeWalker

class BailErrorStrategy : DefaultErrorStrategy() {
override fun recover(recognizer: Parser, e: RecognitionException) {
var context = recognizer.context
while (context != null) {
context.exception = e
context = context.readParent() as ParserRuleContext?
}

throw e
}

override fun recoverInline(recognizer: Parser): Token {
var context = recognizer.context
while (context != null) {
context = context.readParent() as ParserRuleContext?
}
throw InvalidDIDStringError("Invalid Did char found at [line ${recognizer.currentToken?.line}, col ${recognizer.currentToken?.charPositionInLine}] \"${recognizer.currentToken?.text}\"")
}

override fun sync(recognizer: Parser) {}
}

class DIDParser(private var didString: String) {
fun parse(): DID {
var inputStream = CharStreams.fromString(didString)
val lexer = DIDAbnfLexer(inputStream)
val tokenStream = CommonTokenStream(lexer)
val parser = DIDAbnfParser(tokenStream)

parser.errorHandler = BailErrorStrategy()

val context = parser.did()
val listener = DIDParserListener()
ParseTreeWalker().walk(listener, context as ParseTree)

val scheme = listener.scheme ?: throw InvalidDIDStringError("Invalid DID string, missing scheme")
val methodName = listener.methodName ?: throw InvalidDIDStringError("Invalid DID string, missing method name")
val methodId = listener.methodId ?: throw InvalidDIDStringError("Invalid DID string, missing method ID")

return DID(scheme, methodName, methodId)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.iohk.atala.prism.castor

import io.iohk.atala.prism.castor.antlrgrammar.DIDAbnfBaseListener
import io.iohk.atala.prism.castor.antlrgrammar.DIDAbnfParser

class DIDParserListener : DIDAbnfBaseListener() {

var scheme: String? = null
var methodName: String? = null
var methodId: String? = null

override fun exitDid(ctx: DIDAbnfParser.DidContext) {
ctx.SCHEMA()?.let {
scheme = it.text
}
}

override fun exitMethod_name(ctx: DIDAbnfParser.Method_nameContext) {
methodName = ctx.text
}

override fun exitMethod_specific_id(ctx: DIDAbnfParser.Method_specific_idContext) {
methodId = ctx.text
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.iohk.atala.prism.castor

class InvalidDIDStringError(override val message: String) : Exception(message) {
val code: String = "InvalidDIDStringError"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package io.iohk.atala.prism.castor

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class DIDParserTest {

@Test
fun it_should_test_valid_DIDs() {
var didExample1 = "did:aaaaaa:aa:aaa"
var didExample2 = "did:prism01:b2.-_%11:b4._-%11"
var didExample3 =
"did:prism:b6c0c33d701ac1b9a262a14454d1bbde3d127d697a76950963c5fd930605:Cj8KPRI7CgdtYXN0ZXIwEAFKLgoJc2VmsxEiECSTjyV7sUfCr_ArpN9rvCwR9fRMAhcsr_S7ZRiJk4p5k"

var parsedDID1 = DIDParser(didExample1).parse()
var parsedDID2 = DIDParser(didExample2).parse()
var parsedDID3 = DIDParser(didExample3).parse()

assertEquals(parsedDID1.schema, "did")
assertEquals(parsedDID1.method, "aaaaaa")
assertEquals(parsedDID1.methodId, "aa:aaa")

assertEquals(parsedDID2.schema, "did")
assertEquals(parsedDID2.method, "prism01")
assertEquals(parsedDID2.methodId, "b2.-_%11:b4._-%11")

assertEquals(parsedDID3.schema, "did")
assertEquals(parsedDID3.method, "prism")
assertEquals(
parsedDID3.methodId,
"b6c0c33d701ac1b9a262a14454d1bbde3d127d697a76950963c5fd930605:Cj8KPRI7CgdtYXN0ZXIwEAFKLgoJc2VmsxEiECSTjyV7sUfCr_ArpN9rvCwR9fRMAhcsr_S7ZRiJk4p5k"
)
}

@Test
fun it_should_test_invalid_DIDs() {
var didExample1 = "idi:aaaaaa:aa:aaa"
var didExample2 = "did:-prism-:aaaaa:aaaa"
var didExample3 = "did:prism:aaaaaaaaaaa::"
var didExample4 = "did::prism:aaaaaaaaaaa:aaaa"
var didExample5 = "did:prism::aaaaaaaaaaa:bbbb"

val exception = assertFailsWith(
exceptionClass = InvalidDIDStringError::class,
block = {
DIDParser(didExample1).parse()
}
)
assertEquals(exception.code, "InvalidDIDStringError")

val exception2 = assertFailsWith(
exceptionClass = InvalidDIDStringError::class,
block = {
DIDParser(didExample2).parse()
}
)
assertEquals(exception2.code, "InvalidDIDStringError")

val exception3 = assertFailsWith(
exceptionClass = InvalidDIDStringError::class,
block = {
DIDParser(didExample3).parse()
}
)
assertEquals(exception3.code, "InvalidDIDStringError")

val exception4 = assertFailsWith(
exceptionClass = InvalidDIDStringError::class,
block = {
DIDParser(didExample4).parse()
}
)
assertEquals(exception4.code, "InvalidDIDStringError")

val exception5 = assertFailsWith(
exceptionClass = InvalidDIDStringError::class,
block = {
DIDParser(didExample5).parse()
}
)
assertEquals(exception5.code, "InvalidDIDStringError")
}
}
Loading

0 comments on commit 0d7dac1

Please sign in to comment.