*Proposal*: A mechanism to extract variables from ...
# language-proposals
j
Proposal: A mechanism to extract variables from Strings converting Strings to StringTemplates at compile time Example:
Copy code
val foo = """
Hello World from ${firstName} ${lastName}
""".toTemplate()
foo
now contains a StringTemplate which contains a list of variables, firstName and lastName as well as parts of the String:
["Hello World from ", firstName, " ", lastName, "\n"]
Use case 1 on the JVM, SQL queries:
Copy code
val simpleQuery = """
SELECT a, b, c FROM table WHERE foo = $value1 AND bar = ${value2 + 1}
""".toTemplate()
by doing a toTemplate() and replacing all variables directly with ?, you avoid SQL injection attacks while also simplifying having to track how many
?
you've added to the String and how many values you need to provide (especially when building more complex queries that requires concatenating clauses depending on what filters are switched on). If a SQL driver now only accepts a StringTemplate instead of a String for a query, there's very little chance of a dodgy value slipping into the SQL query via ${dodogyValue} whereas there's no way currently to stop someone from putting variables directly into a String and potentially opening a SQL Injection attack vector. Use case 2 that spans JVM + JS:
Copy code
val html = """
<html>
  <head>
    ...
  </head>
  <body>
    Hello World from $firstName $lastName
  </body>
</html>
""".toTemplate()
This now allows me to serve that HTML server-side with firstName and lastName already filled in (great for building sites that requires SEO)
Copy code
<html>
  <head>
    ...
  </head>
  <body>
    Hello World from Piet Venter
  </body>
</html>
but I can also generate a JavaScript ES6 String Template that can be re-used on the web to re-render the DOM if one of those values change client-side:
Copy code
let template = `<html>
  <head>
    ...
  </head>
  <body>
    Hello World from ${firstName} ${lastName}
  </body>
</html>`;
This ES6 template way of doing things is being used by frameworks such as uhtml, lit-html, lit-element and many more and if you're using those with a JVM backend, there's no easy way to get server-side rendering without writing the templates twice, once for backend rendering and once for client rendering. Isomorphic applications with such ES6 frameworks are only supported with a JavaScript backend; with a String.toTemplate() compile-time helper, it becomes possible to build those templates in a Multiplatform way that can be used for the initial server-side rendering and then re-using those templates client-side. XSS attacks can also be mitigated by stripping the ${} from the String and allowing the framework to escape anything that could be considered dangerous and using the positions array to insert those strings before rendering it.
Copy code
val securedString = stringTemplate.mapIndexed { index, part -> if (index.isEven()) part else escape(part.toString()) }.joinToString()
Use case 3: Building re-usable templates that can be used with other services Something like MailChimp might want you to use %value for variables while SendGrid might want a different format for sending emails. I can now re-use my StringTemplate and create extension functions to convert these StringTemplates to something that other services might want to use, StringTemplate.toMailChimpFormat() or StringTemplate.toSendGridFormat() Or if you are sending an email locally where you have all the data, you can just fill in those variables yourself to convert that StringTemplate to a String again. Suggested data format for StringTemplate: StringTemplate can be as simple as a value class that contains the String parts and Value parts all mixed together:
Copy code
@JvmInline
value class StringTemplate(private val value: List<Any?>)
Considerations: 1: Doing
"""    ...    """.trimIndent().toTemplate()
should trim the indent without replacing the template variables after which toTemplate() gets to strip the template variables 2: StringTemplate + StringTemplate should concatenate their individual lists of String parts and Values which would return a new StringTemplate with Strings and Values combined 3: It might be beneficial to adjust Dukat to convert tag-functions from JavaScript into regular Kotlin functions that takes a StringTemplate as param instead of the vararg thing it currently generates that's not really usable in KotlinJS 4: toString() can concatenate all the String parts and values to return a value as if it was never broken up. 5: Other platforms: since StringTemplate is just a List<Any?>, it shouldn't be an issue when interopting with other languages 6: String + StringTemplate should not be allowed and instead it should be encouraged to either explicitly convert String.toTemplate() or StringTemplate.toString() unless somebody explicitly overrides the plus operator to allow this. 7: Shorthand: instead of writing toTemplate() everywhere, maybe there's a nicer way to accomplish this, maybe we can use the Spread Operator here,
*" My String Template ${...} "
// converts this to a StringTemplate at compile time 8: Considering that we'll need to distinguish between what is a static String and what is a Value, there are two approaches here 8.1: wrap Strings and Values with some value class (personally I think this is unnecessary) 8.2: When splitting the String, make sure that it's always following the format String, Value, String, Value, String, ...
""" Hello $foo$bar """
would then split to
[" Hello ", foo, "", bar, " "]
which means two variables that are next to each other will be separated with an empty string and the beginning and end of the list of values will always be a String even if it's an empty String 9: Value classes would solve the "proper escaping of expressions" ticket,
value class HTML(val template: StringTemplate)
, but this can be implemented by each framework separately and should not be part of the standard library. Inside the value class, the toString can be overwritten which can then call the correct escape function
Copy code
fun toString() = template.mapIndexed { index, part -> if (index.isEven()) part else htmlEscape(part.toString()) }.joinToString()
Copy code
val safeHTML: String = HTML("""
  <strong>$foo</strong>$bar
""").toString()
EDIT: submitted this in YouTrack https://youtrack.jetbrains.com/issue/KT-51481
👍 1
j
Both those would be covered by this language proposal. What is the next step, do I write a KEEP for this proposal ?
e
at this point, KEEP is only for language changes that are backed by Kotlin maintainers. all other discussion happens in YouTrack. I believe your best code of action is to update those YouTrack tickets
(or possibly file new ones, but I think your proposal fits into the existing ones well)
j
ok, then I'll write a proper YouTrack and point to each of those tickets
j
I would also link to the work on Java in this space which looks exceedingly similar to your proposal
1
👍 1
j
I'm not aware of what's happening in the Java space at the moment, but I'll happily add a link to the proposal if you have one
e
oh too late to move it now, but KTIJ is not the right project, it should be KT.
j
I can still recreate it and delete the old one