-
Notifications
You must be signed in to change notification settings - Fork 157
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support reading and writing Zipline APIs as TOML (#1051)
I didn't introduce a dependency on a 3rd-party TOML library. I'm open to this, but it might make it difficult to write comments. (I'm less concerned with reading comments.) I'm also anxious about how any dependency will interact with Gradle's classpath.
- Loading branch information
1 parent
fd33c34
commit 24fbc30
Showing
6 changed files
with
533 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
30 changes: 30 additions & 0 deletions
30
zipline-kotlin-plugin/src/main/kotlin/app/cash/zipline/api/toml/TomlZiplineApi.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/* | ||
* Copyright (C) 2023 Cash App | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package app.cash.zipline.api.toml | ||
|
||
data class TomlZiplineApi( | ||
val services: List<TomlZiplineService>, | ||
) | ||
|
||
data class TomlZiplineService( | ||
val name: String, | ||
val functions: List<TomlZiplineFunction>, | ||
) | ||
|
||
data class TomlZiplineFunction( | ||
val leadingComment: String, | ||
val id: String, | ||
) |
206 changes: 206 additions & 0 deletions
206
zipline-kotlin-plugin/src/main/kotlin/app/cash/zipline/api/toml/ZiplineApiTomlReader.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
/* | ||
* Copyright (C) 2023 Cash App | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package app.cash.zipline.api.toml | ||
|
||
import okio.BufferedSource | ||
import okio.ByteString.Companion.encodeUtf8 | ||
import okio.IOException | ||
import okio.Options | ||
|
||
fun BufferedSource.readZiplineApi(): TomlZiplineApi { | ||
return TomlZiplineApiReader(this).readServices() | ||
} | ||
|
||
/** | ||
* Super-limited reader for the tiny subset of TOML we use for Zipline API files. | ||
* | ||
* Among other things, this doesn't support: | ||
* | ||
* - keys or values not within a table | ||
* - keys that aren't `functions` | ||
* - values that aren't arrays of strings | ||
* - escaped string contents | ||
* | ||
* But it does capture comments. | ||
*/ | ||
internal class TomlZiplineApiReader( | ||
private val source: BufferedSource, | ||
) { | ||
fun readServices(): TomlZiplineApi { | ||
val services = mutableListOf<TomlZiplineService>() | ||
|
||
while (true) { | ||
readComment() | ||
|
||
val token = source.select(readServicesToken) | ||
when { | ||
// [com.example.SampleService] | ||
token == readServicesTokenOpenBrace -> { | ||
val serviceName = readTableHeader() | ||
services += readService(serviceName) | ||
} | ||
source.exhausted() -> break | ||
else -> throw IOException("expected '['") | ||
} | ||
} | ||
|
||
return TomlZiplineApi(services) | ||
} | ||
|
||
private fun readService(serviceName: String): TomlZiplineService { | ||
val functions = mutableListOf<TomlZiplineFunction>() | ||
|
||
while (true) { | ||
readComment() | ||
|
||
when (source.select(readServiceToken)) { | ||
// functions = [ ... ] | ||
readServicesTokenFunctions -> { | ||
skipWhitespace() | ||
if (source.select(equals) == -1) throw IOException("expected '='") | ||
skipWhitespace() | ||
functions += readFunctions() | ||
} | ||
else -> break | ||
} | ||
} | ||
|
||
return TomlZiplineService( | ||
name = serviceName, | ||
functions = functions, | ||
) | ||
} | ||
|
||
private fun readFunctions(): List<TomlZiplineFunction> { | ||
val result = mutableListOf<TomlZiplineFunction>() | ||
|
||
readComment() | ||
if (source.select(openBrace) == -1) throw IOException("expected '['") | ||
|
||
while (true) { | ||
val comment = readComment() | ||
when (source.select(readFunctionToken)) { | ||
readFunctionQuote -> { | ||
val functionId = readString() | ||
skipWhitespace() | ||
result += TomlZiplineFunction(comment ?: "", functionId) | ||
when (source.select(afterFunctionToken)) { | ||
afterFunctionComma -> Unit | ||
afterFunctionCloseBrace -> break | ||
else -> throw IOException("expected ',' or ']'") | ||
} | ||
} | ||
readFunctionCloseBrace -> break | ||
else -> throw IOException("expected '\"' or ']'") | ||
} | ||
} | ||
|
||
return result | ||
} | ||
|
||
private fun readTableHeader(): String { | ||
val closeBrace = source.indexOf(']'.code.toByte()) | ||
if (closeBrace == -1L) throw IOException("unterminated '['") | ||
val result = source.readUtf8(closeBrace) | ||
require(source.readByte() == ']'.code.toByte()) | ||
return result | ||
} | ||
|
||
private fun readString(): String { | ||
val closeQuote = source.indexOf('"'.code.toByte()) | ||
if (closeQuote == -1L) throw IOException("unterminated '\"'") | ||
val result = source.readUtf8(closeQuote) | ||
require(source.readByte() == '"'.code.toByte()) | ||
return result | ||
} | ||
|
||
/** Read a potentially multi-line comment as a single string. */ | ||
private fun readComment(): String? { | ||
var result: StringBuilder? = null | ||
|
||
while (true) { | ||
skipWhitespace() | ||
if (source.select(comment) == -1) break | ||
|
||
if (result == null) { | ||
result = StringBuilder() | ||
} else { | ||
result.append("\n") | ||
} | ||
|
||
val line = source.readUtf8Line() ?: "" | ||
result.append(line.trimEnd()) | ||
} | ||
|
||
return result?.toString() | ||
} | ||
|
||
private fun skipWhitespace() { | ||
while (source.select(whitespace) != -1) { | ||
// Skip. | ||
} | ||
} | ||
|
||
private companion object { | ||
val readServicesToken = Options.of( | ||
"[".encodeUtf8(), | ||
) | ||
|
||
const val readServicesTokenOpenBrace = 0 | ||
|
||
val readServiceToken = Options.of( | ||
"functions".encodeUtf8(), | ||
) | ||
|
||
const val readServicesTokenFunctions = 0 | ||
|
||
val readFunctionToken = Options.of( | ||
"\"".encodeUtf8(), | ||
"]".encodeUtf8(), | ||
) | ||
|
||
const val readFunctionQuote = 0 | ||
const val readFunctionCloseBrace = 1 | ||
|
||
val afterFunctionToken = Options.of( | ||
",".encodeUtf8(), | ||
"]".encodeUtf8(), | ||
) | ||
|
||
const val afterFunctionComma = 0 | ||
const val afterFunctionCloseBrace = 1 | ||
|
||
val whitespace = Options.of( | ||
" ".encodeUtf8(), | ||
"\t".encodeUtf8(), | ||
"\r".encodeUtf8(), | ||
"\n".encodeUtf8(), | ||
) | ||
|
||
val comment = Options.of( | ||
"# ".encodeUtf8(), // Prefer to skip a space after a comment. | ||
"#".encodeUtf8(), | ||
) | ||
|
||
val equals = Options.of( | ||
"=".encodeUtf8(), | ||
) | ||
|
||
val openBrace = Options.of( | ||
"[".encodeUtf8(), | ||
) | ||
} | ||
} |
64 changes: 64 additions & 0 deletions
64
zipline-kotlin-plugin/src/main/kotlin/app/cash/zipline/api/toml/ZiplineApiTomlWriter.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
/* | ||
* Copyright (C) 2023 Cash App | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package app.cash.zipline.api.toml | ||
|
||
import okio.BufferedSink | ||
|
||
fun BufferedSink.writeZiplineApi(api: TomlZiplineApi) { | ||
TomlZiplineApiWriter(this).writeApi(api) | ||
} | ||
|
||
/** | ||
* Super-limited writer for the tiny subset of TOML we use for Zipline API files. | ||
* | ||
* This doesn't support strings that require character escapes. | ||
*/ | ||
internal class TomlZiplineApiWriter( | ||
private val sink: BufferedSink, | ||
) { | ||
fun writeApi(api: TomlZiplineApi) { | ||
var first = true | ||
for (service in api.services) { | ||
if (!first) sink.writeUtf8("\n") | ||
first = false | ||
|
||
writeService(service) | ||
} | ||
} | ||
|
||
private fun writeService(service: TomlZiplineService) { | ||
sink.writeUtf8("[").writeUtf8(service.name).writeUtf8("]\n") | ||
sink.writeUtf8("\n") | ||
sink.writeUtf8("functions = [\n") | ||
var first = true | ||
for (function in service.functions) { | ||
if (!first) sink.writeUtf8("\n") | ||
first = false | ||
|
||
writeFunction(function) | ||
} | ||
sink.writeUtf8("]\n") | ||
} | ||
|
||
private fun writeFunction(function: TomlZiplineFunction) { | ||
val comment = function.leadingComment | ||
if (comment.isNotEmpty()) { | ||
sink.writeUtf8(" # ").writeUtf8(comment.replace("\n", "\n # ")).writeUtf8("\n") | ||
} | ||
|
||
sink.writeUtf8(" \"").writeUtf8(function.id).writeUtf8("\",\n") | ||
} | ||
} |
Oops, something went wrong.