Support typed Kotlin imports for SSD records
ober
ee505be9053a52fa7bfa07270b2395230407b6b1
--- a/README.md +++ b/README.md @@ -122,6 +122,17 @@ app spec. `typed-kotlin-file` emits a normal Typed Jerboa `typed-library` through the upstream `(jerboa typed kotlin)` backend. Use it for new generated Kotlin instead of `kotlin-file`, `kotlin-file-lines`, or `kotlin-source-dir`. +When generated records need JVM/Android types, add structured imports before the +library form: + +```scheme +(typed-kotlin-file "com/example/Pick.kt" + (kotlin-imports (android net Uri)) + (typed-library (com example) + (export make-Pick Pick? Pick-uri) + (type Uri) + (record Pick ((uri : Uri))))) +``` `client` expands a named generator-owned template from `templates/<name>.ss`. This is the preferred bridge for larger apps while the higher-level Android DSL --- a/jandroid.ss +++ b/jandroid.ss @@ -1,6 +1,8 @@ (import (jerboa prelude) (only (chezscheme) file-directory?) (jerboa typed kotlin) + (jerboa typed kotlin ast) + (jerboa typed parser) (jandroid secure-output)) (def *max-copy-files* 10000) @@ -792,23 +794,67 @@ (error 'kotlin-file "expected `(kotlin-file path content)` or `(kotlin-file-lines path (line ...))`" file-form)))) (kotlin-files spec))) +(def (symbol-list? value) + (and (list? value) + (not (null? value)) + (every symbol? value))) + +(def (typed-kotlin-imports-form? value) + (and (pair? value) + (eq? (car value) 'kotlin-imports))) + +(def (typed-kotlin-imports import-form) + (unless (typed-kotlin-imports-form? import-form) + (error 'typed-kotlin-file "expected `(kotlin-imports ((package path) ...))`" + import-form)) + (map + (lambda (path) + (unless (symbol-list? path) + (error 'typed-kotlin-file "Kotlin import path must be a non-empty symbol list" + path)) + (make-kt-import path #f)) + (cdr import-form))) + +(def (typed-kotlin-file-parts file-form) + (cond + [(and (= (length file-form) 3) + (string? (cadr file-form)) + (pair? (caddr file-form)) + (eq? (car (caddr file-form)) 'typed-library)) + (values (cadr file-form) '() (caddr file-form))] + [(and (= (length file-form) 4) + (string? (cadr file-form)) + (typed-kotlin-imports-form? (caddr file-form)) + (pair? (cadddr file-form)) + (eq? (car (cadddr file-form)) 'typed-library)) + (values (cadr file-form) + (typed-kotlin-imports (caddr file-form)) + (cadddr file-form))] + [else + (error 'typed-kotlin-file + "expected `(typed-kotlin-file path (typed-library ...))` or `(typed-kotlin-file path (kotlin-imports (...)) (typed-library ...))`" + file-form)])) + +(def (typed-library-form->kotlin-string/imports library-form imports) + (let* ([file (typed-module->kotlin-file (parse-typed-library library-form))] + [merged (make-kt-file + (kt-file-package file) + imports + (kt-file-declarations file))]) + (kotlin-file->string merged))) + (def (write-typed-kotlin-files spec root) (for-each (lambda (file-form) - (unless (and (= (length file-form) 3) - (string? (cadr file-form)) - (pair? (caddr file-form)) - (eq? (car (caddr file-form)) 'typed-library)) - (error 'typed-kotlin-file - "expected `(typed-kotlin-file path (typed-library ...))`" - file-form)) - (unless (string=? (path-extension (cadr file-form)) ".kt") - (error 'typed-kotlin-file "generated Kotlin path must end in .kt" - (cadr file-form))) - (write-generated-file - root - (path-join "app" "src" "main" "java" (cadr file-form)) - (typed-library-form->kotlin-string (caddr file-form)))) + (let-values ([(path imports library-form) + (typed-kotlin-file-parts file-form)]) + (unless (string=? (path-extension path) ".kt") + (error 'typed-kotlin-file "generated Kotlin path must end in .kt" + path)) + (write-generated-file + root + (path-join "app" "src" "main" "java" path) + (typed-library-form->kotlin-string/imports library-form imports)))) (typed-kotlin-files spec))) (def (generated-main-activity? spec relative) --- a/templates/ssd-review.ss +++ b/templates/ssd-review.ss @@ -136,6 +136,49 @@ ((mut files : Int32) (mut bytes : Int))))) + (typed-kotlin-file "com/sfb/ssdreview/SsdPdfChoice.kt" + (kotlin-imports (android net Uri)) + (typed-library (com sfb ssdreview) + (export make-SsdPdfChoice SsdPdfChoice? + SsdPdfChoice-label SsdPdfChoice-uri) + (type Uri) + (record SsdPdfChoice + ((label : String) + (uri : Uri))))) + + (typed-kotlin-file "com/sfb/ssdreview/TruthStoreRecords.kt" + (kotlin-imports (java io File) (java net URL)) + (typed-library (com sfb ssdreview) + (export make-RemoteConfig RemoteConfig? + RemoteConfig-apiUrl RemoteConfig-spkiSha256 RemoteConfig-bearerToken + make-StagedZipEntry StagedZipEntry? + StagedZipEntry-name StagedZipEntry-file StagedZipEntry-modified + make-TruthIndexEntry TruthIndexEntry? + TruthIndexEntry-file TruthIndexEntry-sourceKey + TruthIndexEntry-sourceName TruthIndexEntry-canonicalSourceName + TruthIndexEntry-page TruthIndexEntry-dpi TruthIndexEntry-groupCount + TruthIndexEntry-truthTime) + (type File) + (type URL) + (type Int32) + (record RemoteConfig + ((apiUrl : URL) + (spkiSha256 : (Nullable Bytes)) + (bearerToken : String))) + (record StagedZipEntry + ((name : String) + (file : File) + (modified : Int))) + (record TruthIndexEntry + ((file : File) + (sourceKey : String) + (sourceName : String) + (canonicalSourceName : String) + (page : Int32) + (dpi : Int32) + (groupCount : Int32) + (truthTime : Int))))) + (typed-kotlin-file "com/sfb/ssdreview/Guess.kt" (typed-library (com sfb ssdreview) (export make-Guess Guess? Guess-label Guess-boxType Guess-reason) @@ -2125,8 +2168,6 @@ " private const val CLIENT_LOG_CRASH_TIMEOUT_MS = 1500" " private const val CLIENT_LOG_STACK_CHARS = 12000" " }" - "" - " private data class SsdPdfChoice(val label: String, val uri: Uri)" "}" )) (kotlin-file-lines "com/sfb/ssdreview/Models.kt" @@ -3614,25 +3655,6 @@ " private const val MAX_AUTH_TOKEN_CHARS = 4096" " }" "" - " private data class RemoteConfig(" - " val apiUrl: URL," - " val spkiSha256: ByteArray?," - " val bearerToken: String" - " )" - "" - " private data class StagedZipEntry(val name: String, val file: File, val modified: Long)" - "" - " private data class TruthIndexEntry(" - " val file: File," - " val sourceKey: String," - " val sourceName: String," - " val canonicalSourceName: String," - " val page: Int," - " val dpi: Int," - " val groupCount: Int," - " val truthTime: Long" - " )" - "" " private val root = File(context.filesDir, \"ssd_review\")" " private val groundTruthDir = File(root, \"ground_truth\")" " private val learnedDir = File(root, \"learned\")" @@ -3674,7 +3696,7 @@ " }" " }" " }" - " return best?.let { JSONObject(readLocalText(it.file)) }" + " return best?.let { JSONObject(readLocalText(it._file)) }" " }" "" " @Synchronized" @@ -3700,7 +3722,7 @@ " val source = truth.optJSONObject(\"source\") ?: JSONObject()" " val sourceName = sourceBaseName(source.optString(\"name\", source.optString(\"path\")))" " TruthIndexEntry(" - " file = file," + " _file = file," " sourceKey = truth.optString(\"source_key\")," " sourceName = sourceName," " canonicalSourceName = canonicalSourceName(sourceName)," @@ -4313,7 +4335,7 @@ "" " private fun installStagedZipEntry(entry: StagedZipEntry): Boolean {" " val destination = containedImportPath(entry.name)" - " val bytes = entry.file.inputStream().use { readBoundedBytes(it, MAX_ZIP_ENTRY_BYTES) }" + " val bytes = entry._file.inputStream().use { readBoundedBytes(it, MAX_ZIP_ENTRY_BYTES) }" " return if (entry.name.endsWith(\".jsonl\")) {" " val before = if (destination.exists()) readLocalText(destination) else \"\"" " mergeJsonLines(destination, bytes.decodeToString())"