From 0d7dac11265bc7c3c5abc929f2e5b39e925243cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Rib=C3=B3?= Date: Sat, 21 Jan 2023 23:12:30 +0100 Subject: [PATCH] feat(didparser): Adding amtlr4 grammar did parser with specification 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 --- build.gradle.kts | 1 + castor/build.gradle.kts | 71 +++++++++++++++- castor/src/commonAntlr/antlr/DIDAbnf.g4 | 35 ++++++++ castor/src/commonAntlr/antlr/DIDUrlAbnf.g4 | 61 ++++++++++++++ .../io/iohk/atala/prism/castor/DIDParser.kt | 57 +++++++++++++ .../atala/prism/castor/DIDParserListener.kt | 25 ++++++ .../prism/castor/InvalidDIDStringError.kt | 5 ++ .../iohk/atala/prism/castor/DIDParserTest.kt | 84 +++++++++++++++++++ settings.gradle.kts | 3 + 9 files changed, 340 insertions(+), 2 deletions(-) create mode 100644 castor/src/commonAntlr/antlr/DIDAbnf.g4 create mode 100644 castor/src/commonAntlr/antlr/DIDUrlAbnf.g4 create mode 100644 castor/src/commonMain/kotlin/io/iohk/atala/prism/castor/DIDParser.kt create mode 100644 castor/src/commonMain/kotlin/io/iohk/atala/prism/castor/DIDParserListener.kt create mode 100644 castor/src/commonMain/kotlin/io/iohk/atala/prism/castor/InvalidDIDStringError.kt create mode 100644 castor/src/commonTest/kotlin/io/iohk/atala/prism/castor/DIDParserTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index ea101b1ba..64e60e962 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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") } } diff --git a/castor/build.gradle.kts b/castor/build.gradle.kts index d8a4be129..d3961cb7c 100644 --- a/castor/build.gradle.kts +++ b/castor/build.gradle.kts @@ -64,10 +64,19 @@ 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 { @@ -75,7 +84,12 @@ kotlin { 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") @@ -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 { @@ -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 { moduleName.set(project.name) @@ -150,3 +186,34 @@ tasks.withType { // } // } // } + +val antlrGenerationTask by tasks.register("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) +} diff --git a/castor/src/commonAntlr/antlr/DIDAbnf.g4 b/castor/src/commonAntlr/antlr/DIDAbnf.g4 new file mode 100644 index 000000000..3036e6bd1 --- /dev/null +++ b/castor/src/commonAntlr/antlr/DIDAbnf.g4 @@ -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 : '_'; + diff --git a/castor/src/commonAntlr/antlr/DIDUrlAbnf.g4 b/castor/src/commonAntlr/antlr/DIDUrlAbnf.g4 new file mode 100644 index 000000000..40d7f3f37 --- /dev/null +++ b/castor/src/commonAntlr/antlr/DIDUrlAbnf.g4 @@ -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)*; diff --git a/castor/src/commonMain/kotlin/io/iohk/atala/prism/castor/DIDParser.kt b/castor/src/commonMain/kotlin/io/iohk/atala/prism/castor/DIDParser.kt new file mode 100644 index 000000000..e9624b8e7 --- /dev/null +++ b/castor/src/commonMain/kotlin/io/iohk/atala/prism/castor/DIDParser.kt @@ -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) + } +} diff --git a/castor/src/commonMain/kotlin/io/iohk/atala/prism/castor/DIDParserListener.kt b/castor/src/commonMain/kotlin/io/iohk/atala/prism/castor/DIDParserListener.kt new file mode 100644 index 000000000..6da413557 --- /dev/null +++ b/castor/src/commonMain/kotlin/io/iohk/atala/prism/castor/DIDParserListener.kt @@ -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 + } +} diff --git a/castor/src/commonMain/kotlin/io/iohk/atala/prism/castor/InvalidDIDStringError.kt b/castor/src/commonMain/kotlin/io/iohk/atala/prism/castor/InvalidDIDStringError.kt new file mode 100644 index 000000000..8cf9a8108 --- /dev/null +++ b/castor/src/commonMain/kotlin/io/iohk/atala/prism/castor/InvalidDIDStringError.kt @@ -0,0 +1,5 @@ +package io.iohk.atala.prism.castor + +class InvalidDIDStringError(override val message: String) : Exception(message) { + val code: String = "InvalidDIDStringError" +} diff --git a/castor/src/commonTest/kotlin/io/iohk/atala/prism/castor/DIDParserTest.kt b/castor/src/commonTest/kotlin/io/iohk/atala/prism/castor/DIDParserTest.kt new file mode 100644 index 000000000..39c364848 --- /dev/null +++ b/castor/src/commonTest/kotlin/io/iohk/atala/prism/castor/DIDParserTest.kt @@ -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") + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 8cb934f53..b1deaada1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,6 +3,8 @@ pluginManagement { gradlePluginPortal() mavenCentral() google() + maven("https://plugins.gradle.org/m2/") + maven("https://jitpack.io") } } @@ -17,6 +19,7 @@ buildscript { maven { url = uri("https://maven.pkg.jetbrains.space/public/p/kotlinx-coroutines/maven") } + maven("https://jitpack.io") } dependencies {