{"id":17792907,"url":"https://github.com/exoquery/terpal","last_synced_at":"2025-04-09T20:42:55.681Z","repository":{"id":222390657,"uuid":"757133734","full_name":"ExoQuery/Terpal","owner":"ExoQuery","description":"Typed String Interpolation for Kotlin","archived":false,"fork":false,"pushed_at":"2024-12-15T06:52:24.000Z","size":569,"stargazers_count":43,"open_issues_count":4,"forks_count":3,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-03-23T22:38:09.115Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ExoQuery.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-02-13T21:42:09.000Z","updated_at":"2025-01-29T23:29:44.000Z","dependencies_parsed_at":"2024-02-25T07:02:55.872Z","dependency_job_id":"2334dbc6-11b0-4fee-9f44-7f61b49c50ca","html_url":"https://github.com/ExoQuery/Terpal","commit_stats":null,"previous_names":["deusaquilus/terpal","exoquery/terpal"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExoQuery%2FTerpal","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExoQuery%2FTerpal/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExoQuery%2FTerpal/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExoQuery%2FTerpal/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ExoQuery","download_url":"https://codeload.github.com/ExoQuery/Terpal/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248110233,"owners_count":21049456,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-27T11:02:05.832Z","updated_at":"2025-04-09T20:42:55.673Z","avatar_url":"https://github.com/ExoQuery.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Terpal - Typed String Interpolation for Kotlin\n\n\u003e NOTE: This repo is the Terpal Compiler Plugin. The Terpal-SQL project that uses this plugin is [here](https://github.com/deusaquilus/terpal-sql).\n\nTerpal is a Kotlin compiler-plugin that allows capturing the \"the $dollar $sign $varaibles\" of a string before they are spliced back into the string.\n\n### Why do we Need This?\n\nKotlin's text-interpolation currently cannot be customized at all. It is impossible to capture the value of \"the $dollar $sign $varaibles\" \nbefore they are spliced into the surrounding string. This makes Kotlin miss out on some very powerful tools.\n\nFor example, in Scala, libraries like Doobie and Quill use the string-prefix \"sql\" to specify SQL snippets such as:\n\n```scala\nsql\"SELECT * FROM users WHERE id = $id AND name = $name\"\n```\nIn Kotlin the value of `id` would be spliced directly into the string e.g. \"SELECT * FROM users WHERE id = 1234 AND name = 'Joe'\"\nhowever this is highly problematic as it opens up the possibility of SQL injection attacks for example:\n```scala\n\"SELECT * FROM users WHERE id = 1234; DROP TABLE users; AND name = 'Joe'\"\n```\n\nScala's string interpolation allows the library to know that \"id\" is a variable and should be escaped before \nsplicing it into the string. It uses the following API:\n\n```scala\nimplicit class SqlInterpolator(val sc: StringContext) extends AnyVal {\n  // Values of $dollar_sign_variables i.e. `id`, `name` are this list i.e. [1234, \"Joe\"]\n  def sql(params: Any*): PreparedStatement = {\n    // The string-parts [\"SELECT * FROM users WHERE id = \", \" AND name = \" and \"\"] are this list\n    val stringParts: List[String] = sc.parts\n    ...\n  }\n}\n```\n\nThis is a very powerful feature that allows libraries to create DSLs that are both safe and easy to use.\nSadly Kotlin does not have it.\n\n### The Solution\n\nTerpal remedies this problem with a compiler-plugin that contains these exact semantics.\nUsing Terpal, you would write the above as the following: \n```kotlin\nclass SqlInterpolator(val connection: Connection): Interpolator\u003cAny, PreparedStatement\u003e {\n  // Parts is [\"SELECT * FROM users WHERE id = \", \" AND name = \", \"\"]\n  // Params is [`id`, `name`] i.e. [1234, \"Joe\"]\n  override fun interpolate(parts: () -\u003e List\u003cString\u003e, params: () -\u003e List\u003cAny\u003e): PreparedStatement {\n    ...\n  }\n}\n\nval sql = SqlInterpolator\u003cAny, PreparedStatement\u003e(connection)\nval (id, name) = 1234 to \"Joe\"\nval stmt = sql(\"SELECT * FROM users WHERE id = $id AND name = $name\")\n// I.e the `sql.invoke(...)` function forwards the parts/params to sql.interpolate\n```\n\nThe actual `interpolate` function could easily be implemented as something like this:\n```\noverride fun interpolate(parts: () -\u003e List\u003cString\u003e, params: () -\u003e List\u003cAny\u003e): PreparedStatement {\n  val stmt = connection.prepareStatement(parts().joinToString(\"?\"))\n  for ((arg, i) \u003c- params().zipWithIndex) {\n    stmt.setObject(i + 1, arg)\n  }\n  return stmt\n}\n```\n\n## Usage\n\n\u003e (UPDATE - This plugin is now **fully approved** and available via plugins.gradle.org)\n\nIn order to use Terpal in your projects, you need to add the following to your `build.gradle.kts`:\n\n\u003e I am currently having an issue publishing to gradle. It should be resolved by tomorrow.\n\n```kotlin\nplugins {\n  kotlin(\"jvm\") version \"2.1.20\"\n  id(\"io.exoquery.terpal-plugin\") version \"2.1.20-2.0.0.PL\"\n}\n\ndependencies {\n  api(\"io.exoquery:terpal-runtime:1.0.6\")\n}\n```\n\nBe sure to include the Gradle Plugin Repository and Maven repos in the `pluginManagement/repositories` block of your `settings.gradle.kts`:\n```kotlin\npluginManagement {\n    ...\n    repositories {\n        gradlePluginPortal()\n        mavenCentral()\n        mavenLocal()\n    }\n}\n```\n\n## Error Handling\n\nIf any of the terms spliced into the Terpal string throw an exception, it will be wrapped into an InterpolationException\nand rethrown. The InterpolationException will contain the original exception as well as some context as to\nthe splice that caused the error:\n\n```kotlin\nval id by lazy { throw Exception(\"This is an exception\") }\nval name = \"Joe\"\nSql(\"SELECT * FROM users WHERE id = $id AND name = $name\")\n// Error in spliced code `id` expression #1 (of 2) at file:///...:72:38\n```\n\nA slightly longer message will be printed if the expression is multiple lines long.\n\n\u003e Note that in some cases the code may not be exactly the same as the original code.\n\u003e In such cases the original code could not be loaded and needed to be retrieved from the kotlin Intermediate Representation.\n\u003e The message would then look like the following:\n\u003e ```\n\u003e // Error in spliced (approximately looking) code `\u003cthis\u003e.\u003cget-id\u003e` expression #1 (of 2) at file:///...:72:38\n\u003e ```\n\n## Other Features\n\nTerpal interpolators can be instantiated classes as well as static ones. For example, the above\ninterpolator could have been written as:\n```kotlin\nobject SqlInterpolator: Interpolator\u003cAny, PreparedStatement\u003e { \n  fun  interpolate(parts: () -\u003e List\u003cString\u003e, params: () -\u003e List\u003cAny\u003e): PreparedStatement {\n    ...\n    return stmt\n  }\n}\n```\n\n## Interpolator Functions\nIn addition to classes/objects, you can use an annotation to assign a function to act as an interpolator:\nfor example:\n\n```kotlin\n@InterpolatorFunction\u003cSqlInterpolator\u003e(SqlInterpolator::class)\nfun sql2(sqlString: String): PreparedStatement = interpolatorBody()\n\n// usage\nsql2(\"SELECT * FROM users WHERE id = $id AND name = $name\")\n```\n\nBy using this combined with an string extension function, you can create a very compact DSL for interpolation:\n```kotlin\n@InterpolatorFunction\u003cSqlInterpolator\u003e(SqlInterpolator::class)\noperator fun String.unaryPlus() = interpolatorBody()\n\n// usage\n+\"SELECT * FROM users WHERE id = $id AND name = $name\"\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexoquery%2Fterpal","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fexoquery%2Fterpal","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexoquery%2Fterpal/lists"}