Parser Generator (一)

KotlinParserGenerator

我们先从 kotlin 的 parser 讲起,这边会顺便带到一些 KotlinPoet 的进阶用法。我们目标是读取 annotation 的资讯产生对应的 parser ,以下面这组的 annotation 为例:

@RssTag(name = "channel")
data class TestRssData(
    val title: String?,
    @RssTag
    val link: String?,
    val textInput: MyTextInput?,
    @RssTag(name = "item")
    val list: List<RssItem>,
    @RssTag(name = "category")
    val categories: List<TestCategory>,
    val skipDays: SkipDays?,
    val ttl: Long?,
    val image: TestImage?,
    val cloud: TestCloud?,
): Serializable

这个类别包含的子结构就不在这边一一列出,原始码可以参考这里。在写 generator 之前,我们要先规划产生出来程序码会长什麽样子。以上面的例子,我们可以想像会有个 TestRssDataParser 提供一系列的 function 来从目标类别 ( TestRssData ) 爬出资讯, 而它包含的子结构,可以透过子结构本身再产生一个类别提供 function 去负责爬该类别的资讯。举例来说,负责爬 TestRssData 资讯的是 TestRssDataParser ,而里面会用到子结构 RssItem 的资讯,则是产生另一个 RssItemParser 去爬取,这个概念将可以套用在其他所有的 generator 上面。

让我们一步步拆解 TestRssDataParser 来讲解怎麽做的,首先我们先订出最重要的 parse function ,输入是 XML 字串,输出是被标 annotation 的 data class ,在这边是 TestRssData

object TestRssDataParser {
  fun parse(xml: String): TestRssData {
     val builder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
     val document = builder.parse(xml.byteInputStream())
     document.documentElement.normalize()
     val nodeList = document.getElementsByTagName("channel")
     var result: TestRssData? = null

     if (nodeList?.length == 1) {
    		val element = nodeList.item(0) as? Element
    		element?.let {
    			result = it.getChannel()
    		}
     }
     return result ?: throw IllegalStateException("No valid channel tag in the RSS feed.")
  }
}

上述的程序码有没有很熟悉啊?它就是前面在讲 DOM parser 的时候的作法,一样的逻辑搬过来,只是现在我们要用 annotation 给的资讯,接着用 KotlinPoet 把动态产生出来。

const val PARSER_FUNC_NAME = "parse"
const val CHANNEL = "channel"

private val outputClass = ClassName(element.getPackage(), element.simpleName.toString())
private val exceptionClass = ClassName("java.lang", "IllegalStateException")
private val docBuilderFactoryClass = ClassName("javax.xml.parsers", "DocumentBuilderFactory")

fun getParseFuncSpec(): FunSpec {
    return FunSpec.builder(PARSER_FUNC_NAME)
        .addParameter("xml", String::class)
        .addCode(
            """
            | val builder = %4T.newInstance().newDocumentBuilder()
            | val document = builder.parse(xml.byteInputStream())
            | document.documentElement.normalize()
            | val nodeList = document.getElementsByTagName("%2L")
            | var result: %1T? = null
            |
            | if (nodeList?.length == 1) {
            |${TAB}${TAB}val element = nodeList.item(0) as? Element
            |${TAB}${TAB}element?.let {
            |${TAB}${TAB}${TAB}result = it.getChannel()
            |${TAB}${TAB}}
            | }
            | return result ?: throw %3T("No valid channel tag in the RSS feed.")
            | """.trimMargin(),
            outputClass, CHANNEL, exceptionClass, docBuilderFactoryClass
        )
        .returns(outputClass)
        .build()
}

这个 function 就是产生上方 TestRssDataParser 内的 parse function ,除了设定好输入输出之外,还可以针对程序码内部设定一些动态的 type 参数,也就是在 addCode 里面的 %1T%2L%3T%4T 。1234 分别代表後方带入的参数顺序,对应到 addCode 後方戴的那几个参数outputClassCHANNELexceptionClassdocBuilderFactoryClass 。L 代表字串型态,T 则代表类别。前面用 ClassName 的方式宣告在 KotlinPoet ,在它产生程序码的同时,会帮我们把对应的类别自动 import ,所以我们不用担心会有 import 错误的问题。


<<:  [NestJS 带你飞!] DAY09 - Pipe (上)

>>:  食谱搜寻系统_新增资料excel档

Day 19-infrastructure 也可以 for each 之四:for & dynamic block

这篇是 infrastructure 也可以 for each 第四篇,上次漏发了,今天补发 本章介...

JavaScript学习日记 : Day25 - Set

Set与Map不同再於Set没有key,是指有包含值的特殊集合,且每个值只能出现一次不能重复。 Se...

Day17 Elastic APM (一)

接下来将要来介绍如何运用APM(Application Performance Monitoring...

[Day 44] 心情随笔後台及前台(六) - 心情随笔前台画面

接下来我们要做的是心情随笔前台的画面, 我们要在 app/Http/Controllers/Home...

Day-17 就是要重现这一部!没有极限的 PS2!

在这第六世代的战争中、面对来势汹汹的 DC、SONY 当然也早就有准备、非常机歪的选在 DC 发售的...