diff --git a/src/main/kotlin/fr/postgresjson/connexion/Requester.kt b/src/main/kotlin/fr/postgresjson/connexion/Requester.kt index 75626a4..48eeea4 100644 --- a/src/main/kotlin/fr/postgresjson/connexion/Requester.kt +++ b/src/main/kotlin/fr/postgresjson/connexion/Requester.kt @@ -1,5 +1,6 @@ package fr.postgresjson.connexion +import fr.postgresjson.definition.parse.parseFunction import fr.postgresjson.utils.searchSqlFiles import java.net.URI import fr.postgresjson.definition.Function as DefinitionFunction @@ -48,7 +49,7 @@ class Requester( } fun addFunction(sql: String) { - DefinitionFunction(sql) + parseFunction(sql) .run { toRunnable(connection) } .run { functions[name] = this } } diff --git a/src/main/kotlin/fr/postgresjson/definition/Function.kt b/src/main/kotlin/fr/postgresjson/definition/Function.kt index 1998bf4..ae3ce79 100644 --- a/src/main/kotlin/fr/postgresjson/definition/Function.kt +++ b/src/main/kotlin/fr/postgresjson/definition/Function.kt @@ -1,345 +1,36 @@ package fr.postgresjson.definition -import fr.postgresjson.definition.Parameter.Direction +import fr.postgresjson.definition.Parameter.Direction.IN +import fr.postgresjson.definition.parse.ScriptPart +import fr.postgresjson.definition.parse.trimSpace import java.nio.file.Path -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind.EXACTLY_ONCE -import kotlin.contracts.contract -import kotlin.text.RegexOption.IGNORE_CASE class Function( + override val name: String, + override val parameters: List, + val returns: Returns, override val script: String, override val source: Path? = null, ): Resource, ParametersInterface { - /** - * TEXT, INT, TEXT[], CUSTOM_TYPE => String, Int, List, Any; - * TABLE(id INT, name TEXT) => Object { id: Int; name: String }; - * SETOF TEXT => List; - * MY_TABLE.id%TYPE => Any; - * VOID => null; - */ - val returns: Returns - override val name: String - override val parameters: List - - @JvmInline - private value class ScriptPart(val restOfScript: String) { - fun copy(block: (String) -> String): ScriptPart { - return ScriptPart(block(restOfScript)) - } - - fun removeParentheses(): ScriptPart { - return if (restOfScript.take(1) == "(" && restOfScript.takeLast(1) == ")") { - this.copy { - it.drop(1).dropLast(1) - } - } else { - this - } - } - - fun isEmpty() = restOfScript.isEmpty() - } - - private fun emptyScriptPart(): ScriptPart = ScriptPart("") - - private class NextScript(val value: T, val restOfScript: String) { - val nextScriptPart: ScriptPart = ScriptPart(restOfScript) - fun isLast() = restOfScript == "" - fun isEmptyValue() = value == "" || value == null - } - - /** - * Return the value as ScriptPart - */ - private fun NextScript.valueAsScriptPart(): ScriptPart = ScriptPart(value) - - init { - ScriptPart(script) - .getFunctionOrProcedure().trimSpace().nextScriptPart - .getFunctionName().apply { name = value }.nextScriptPart - .getParameters().apply { parameters = value }.nextScriptPart - .getReturns().apply { returns = value } - - } - - private fun ScriptPart.getFunctionOrProcedure(): NextScript { - val result = """create\s+(?:or\s+replace\s+)?(procedure|function)\s+""" - .toRegex() - .find(restOfScript) - ?: throw FunctionNotFound() - - val rest = result.range.last - .let { cursor -> restOfScript.drop(cursor + 1) } - - return NextScript( - result.groups[1]!!.value, - rest - ) - } - - private fun ScriptPart.getFunctionName(): NextScript { - try { - return getNextScript { status.isNotEscaped() && afterBeginBy("(", " ", "\n") } - } catch (e: NameMalformed) { - throw FunctionNameMalformed(null, e) - } - } - - @OptIn(ExperimentalContracts::class) - private inline fun ScriptPart.change(block: String.() -> String): ScriptPart { - contract { - callsInPlace(block, EXACTLY_ONCE) - } - return ScriptPart(restOfScript.run(block)) - } - - data class Status( - var doubleQuoted: Boolean = false, // " - var simpleQuoted: Boolean = false, // ' - var parentheses: Int = 0, // () - var brackets: Int = 0, // [] - var braces: Int = 0, // {} - ) { - fun isQuoted(): Boolean = doubleQuoted || simpleQuoted - fun isNotQuoted(): Boolean = !isQuoted() - fun isNotEscaped(): Boolean = isNotQuoted() && parentheses == 0 && brackets == 0 && braces == 0 - } - - data class Context( - val index: Int, - val currentChar: Char, - val status: Status, - val script: String, - ) { - fun afterBeginBy(vararg texts: String): Boolean = texts.any { - script.substring(index + 1).take(it.length) == it - } - - val nextChar: Char? get() = script.substring(index + 1).getOrNull(0) - } - - /** - * Get next part of script. - * You can define a list of characters that end the part of script. Like `(` or space. - */ - private fun ScriptPart.getNextScript(isEnd: Context.() -> Boolean = { false }): NextScript { - val status = Status() - - fun String.unescape(): String { - val first = take(1) - val last = takeLast(1) - return if (first == last && first in listOf("\"", "'")) { - drop(1).dropLast(1).replace("$first$first", first) - } else { - this - } - } - - for ((index, c) in restOfScript.withIndex()) { - val nextChar = restOfScript.getOrNull(index + 1) - val prevChar = restOfScript.getOrNull(index - 1) - if (c == '"' && (nextChar != '"' && prevChar != '"')) { - status.doubleQuoted = !status.doubleQuoted - } else if (c == '\'' && (nextChar != '\'' && prevChar != '\'')) { - status.simpleQuoted = !status.simpleQuoted - } else if (c == '(' && status.isNotQuoted()) { - status.parentheses++ - } else if (c == ')' && status.isNotQuoted()) { - status.parentheses-- - } else if (c == '[' && status.isNotQuoted()) { - status.brackets++ - } else if (c == ']' && status.isNotQuoted()) { - status.brackets-- - } else if (c == '{' && status.isNotQuoted()) { - status.braces++ - } else if (c == '}' && status.isNotQuoted()) { - status.braces-- - } - - if (isEnd(Context(index, c, status.copy(), restOfScript))) { - return NextScript(restOfScript.take(index + 1).unescape(), restOfScript.drop(index + 1)) - } - } - if (status.isNotEscaped()) { - return NextScript(restOfScript.unescape().trim(), "").trimSpace() - } - throw ParseError() - } - - private fun ScriptPart.split(delimiter: String): List { - val parts: MutableList = mutableListOf() - var rest: ScriptPart = this - do { - rest = rest.trimSpace() - .getNextScript { status.isNotEscaped() && currentChar.toString() == delimiter } - .trimSpace() - .also { parts.add(it.valueAsScriptPart().trimSpace().trimEnd(',')) } - .nextScriptPart - } while (!rest.isEmpty()) - - return parts - } - - private fun ScriptPart.getNextInteger(): NextScript { - val trimmed = restOfScript.trimStart { !it.isDigit() } - val digits = trimmed.takeWhile { it.isDigit() } - val restOfScript = trimmed.trimStart { it.isDigit() } - return NextScript(digits.toIntOrNull(), restOfScript).trimSpace() - } - - private fun ScriptPart.getParameters(): NextScript> { - val allParametersScript = this.getNextScript { - currentChar == ')' && status.isNotEscaped() - } - val parameterList: List = allParametersScript - .valueAsScriptPart() - .removeParentheses() - .split(",") - .map { it.toParameter() } - - return NextScript(parameterList, allParametersScript.restOfScript) - } - - private fun ScriptPart.trimSpace(): ScriptPart { - for ((n, char) in restOfScript.withIndex()) { - if (char !in listOf(' ', '\n', '\t')) { - return ScriptPart( - restOfScript.drop(n) - ) - } - } - return ScriptPart(restOfScript) - } - - private fun NextScript.trimSpace(): NextScript { - val spaces = charArrayOf(' ', '\n', '\t') - return trim(chars = spaces) - } - - private fun ScriptPart.trimEnd(vararg chars: Char): ScriptPart { - return this.change { dropLastWhile { it in chars } } - } - - private fun NextScript.trim(vararg chars: Char): NextScript { - return NextScript(value, restOfScript.apply { dropWhile { it in chars } }) - } - - private fun NextScript.changeValue(block: (T) -> T): NextScript { - return NextScript(block(value), restOfScript) - } - - private fun NextScript.changeScript(block: (String) -> String): NextScript { - return NextScript(value, block(restOfScript)) - } - - private fun NextScript.dropOneOf(vararg endTextList: String): NextScript { - return changeScript { script -> - endTextList - .filter { script.startsWith(it) } - .let { script.drop(it.size) } - } - } - - private fun ScriptPart.toParameter(): Parameter { - var script: ScriptPart = this.trimSpace() - return Parameter( - direction = script.getParameterMode().apply { script = nextScriptPart }.value, - name = script.getParameterName().trimSpace().apply { script = nextScriptPart }.value.trim(), - type = script.getParameterType().trimSpace().apply { script = nextScriptPart }.value, - default = script.getParameterDefault().trimSpace().apply { script = nextScriptPart }.value, - ) - } - - private fun ScriptPart.getParameterMode(): NextScript { - return when { - restOfScript.startsWith("inout ", true) -> NextScript(Direction.INOUT, restOfScript.drop("inout ".length)) - restOfScript.startsWith("in ", true) -> NextScript(Direction.IN, restOfScript.drop("in ".length)) - restOfScript.startsWith("out ", true) -> NextScript(Direction.OUT, restOfScript.drop("out ".length)) - else -> NextScript(Direction.IN, restOfScript) - } - } - - private fun ScriptPart.getParameterName(): NextScript { - try { - return getNextScript { afterBeginBy(" ", "\n") } - } catch (e: NameMalformed) { - throw ParameterNameMalformed(null, e) - } - } - - private fun ScriptPart.getParameterType(): NextScript { - val fullType = try { - val endTextList = arrayOf(" default ", "=", ")") - getNextScript { afterBeginBy(texts = endTextList) } - } catch (e: ParseError) { - throw ParameterTypeMalformed(null, e) - } - - var rest: ScriptPart = fullType.valueAsScriptPart() - - val name = rest - .getNextScript { afterBeginBy("(") } - .apply { rest = nextScriptPart } - val precision = rest - .getNextInteger() - .apply { rest = nextScriptPart } - val scale = rest - .getNextInteger() - .apply { rest = nextScriptPart } - - return NextScript( - ParameterType( - name = name.value.trim(), - precision = precision.value, - scale = scale.value - ), fullType.nextScriptPart.restOfScript - ) - } - - private fun ScriptPart.getParameterDefault(): NextScript { - return if (this.isEmpty() || this.restOfScript == ")") { - NextScript(null, "") - } else { - """^(\s*=\s*|\s+default\s+)(.+)\s*$""" - .toRegex(IGNORE_CASE) - .find(restOfScript) - .let { it ?: throw ParameterDefaultMalformed() } - .let { it.groups[2]!!.value } - .let { NextScript(it, "") } - } - } - - /** - * TODO Finalize this - */ - private fun ScriptPart.getReturns(): NextScript { - return NextScript(Returns.Void(), "") - } - - class FunctionNotFound(cause: Throwable? = null): Resource.ParseException("Function not found in script", cause) - class ParameterNotFound(cause: Throwable? = null): Resource.ParseException("Parameter not found in script", cause) - class FunctionNameMalformed(message: String? = null, cause: Throwable? = null): - Resource.ParseException(message ?: "Function name is malformed", cause) - - class ParameterNameMalformed(message: String? = null, cause: Throwable? = null): - Resource.ParseException(message ?: "Parameter name is malformed", cause) - - class ParameterTypeMalformed(message: String? = null, cause: Throwable? = null): - Resource.ParseException(message ?: "Parameter type is malformed", cause) - - class ParameterDefaultMalformed(message: String? = null, cause: Throwable? = null): - Resource.ParseException(message ?: "Parameter default is malformed", cause) - - class NameMalformed(message: String? = null, cause: Throwable? = null): - Resource.ParseException(message ?: "Name is malformed", cause) - - class ParseError(message: String? = null, cause: Throwable? = null): - Resource.ParseException(message ?: "Parsing fail", cause) +// private fun NextScript.changeValue(block: (T) -> T): NextScript { +// return NextScript(block(value), restOfScript) +// } +// +// private fun NextScript.changeScript(block: (String) -> String): NextScript { +// return NextScript(value, block(restOfScript)) +// } +// +// private fun NextScript.dropOneOf(vararg endTextList: String): NextScript { +// return changeScript { script -> +// endTextList +// .filter { script.startsWith(it) } +// .let { script.drop(it.size) } +// } +// } fun getDefinition(): String = parameters - .filter { it.direction == Direction.IN } + .filter { it.direction == IN } .joinToString(", ") { it.type.toString() } .let { "$name ($it)" } @@ -356,44 +47,4 @@ class Function( infix fun `is different from`(other: Function): Boolean { return other.script != this.script } - - sealed class Returns( - val definition: String, - val isSetOf: Boolean, - ) { - class Primitive( - definition: String, - isSetOf: Boolean, - ): Returns(definition, isSetOf) { - val name = definition - .trim('"') - } - - class PrimitiveList( - definition: String, - isSetOf: Boolean, - ): Returns(definition, isSetOf) { - val name = definition - .drop(2) - .trim('"') - } - - class Table( - definition: String, - isSetOf: Boolean, - val parameters: List, - ): Returns(definition, isSetOf) { - class ParameterTable( - override val name: String, - override val type: ParameterType, - ): ParameterSimpleI - } - - class Any( - isSetOf: Boolean, - ): Returns("any", isSetOf) - - class Void: Returns("void", false) - } - } diff --git a/src/main/kotlin/fr/postgresjson/definition/Resource.kt b/src/main/kotlin/fr/postgresjson/definition/Resource.kt index 4a96a9b..8241803 100644 --- a/src/main/kotlin/fr/postgresjson/definition/Resource.kt +++ b/src/main/kotlin/fr/postgresjson/definition/Resource.kt @@ -1,5 +1,6 @@ package fr.postgresjson.definition +import fr.postgresjson.definition.parse.parseFunction import java.io.File import java.net.URL import java.nio.file.Path @@ -23,7 +24,7 @@ sealed interface Resource { Migration(resource, path) } catch (e: ParseException) { try { - Function(resource, path) + parseFunction(resource, path) } catch (e: ParseException) { try { Query(resource, path) diff --git a/src/main/kotlin/fr/postgresjson/definition/Returns.kt b/src/main/kotlin/fr/postgresjson/definition/Returns.kt new file mode 100644 index 0000000..a9900da --- /dev/null +++ b/src/main/kotlin/fr/postgresjson/definition/Returns.kt @@ -0,0 +1,40 @@ +package fr.postgresjson.definition + +sealed class Returns( + val definition: String, + val isSetOf: Boolean, +) { + class Primitive( + definition: String, + isSetOf: Boolean, + ): Returns(definition, isSetOf) { + val name = definition + .trim('"') + } + + class PrimitiveList( + definition: String, + isSetOf: Boolean, + ): Returns(definition, isSetOf) { + val name = definition + .drop(2) + .trim('"') + } + + class Table( + definition: String, + isSetOf: Boolean, + val parameters: List, + ): Returns(definition, isSetOf) { + class ParameterTable( + override val name: String, + override val type: ParameterType, + ): ParameterSimpleI + } + + class Any( + isSetOf: Boolean, + ): Returns("any", isSetOf) + + class Void: Returns("void", false) +} \ No newline at end of file diff --git a/src/main/kotlin/fr/postgresjson/definition/parse/ParsingFunction.kt b/src/main/kotlin/fr/postgresjson/definition/parse/ParsingFunction.kt new file mode 100644 index 0000000..dd59ae6 --- /dev/null +++ b/src/main/kotlin/fr/postgresjson/definition/parse/ParsingFunction.kt @@ -0,0 +1,160 @@ +package fr.postgresjson.definition.parse + +import fr.postgresjson.definition.Function +import fr.postgresjson.definition.Parameter +import fr.postgresjson.definition.Returns.Void +import fr.postgresjson.definition.Parameter.Direction +import fr.postgresjson.definition.Parameter.Direction.IN +import fr.postgresjson.definition.Parameter.Direction.INOUT +import fr.postgresjson.definition.Parameter.Direction.OUT +import fr.postgresjson.definition.ParameterType +import fr.postgresjson.definition.Resource.ParseException +import fr.postgresjson.definition.Returns +import java.nio.file.Path +import kotlin.text.RegexOption.IGNORE_CASE + +internal fun parseFunction(script: String, source: Path? = null): Function { + val name: String + val parameters: List + val returns: Returns + ScriptPart(script) + .getFunctionOrProcedure().trimSpace().nextScriptPart + .getFunctionName().apply { name = value }.nextScriptPart + .getParameters().apply { parameters = value }.nextScriptPart + .getReturns().apply { returns = value } + + return Function(name, parameters, returns, script, source) +} + + +@Throws(FunctionNameMalformed::class) +internal fun ScriptPart.getFunctionName(): NextScript { + try { + return getNextScript { status.isNotEscaped() && afterBeginBy("(", " ", "\n") } + } catch (e: ParseException) { + throw FunctionNameMalformed(this, e) + } +} +internal class FunctionNameMalformed(val script: ScriptPart, cause: Throwable? = null): + ParseException("Function name is malformed", cause) + +@Throws(FunctionNotFound::class) +internal fun ScriptPart.getFunctionOrProcedure(): NextScript { + val result = """create\s+(?:or\s+replace\s+)?(procedure|function)\s+""" + .toRegex() + .find(restOfScript) + ?: throw FunctionNotFound(this) + + val rest = result.range.last + .let { cursor -> restOfScript.drop(cursor + 1) } + + return NextScript( + result.groups[1]!!.value, + rest + ) +} + +internal class FunctionNotFound(val script: ScriptPart): + ParseException("Function not found in script") + +internal fun ScriptPart.getParameters(): NextScript> { + val allParametersScript = this.getNextScript { + currentChar == ')' && status.isNotEscaped() + } + val parameterList: List = allParametersScript + .valueAsScriptPart() + .removeParentheses() + .split(",") + .map { it.toParameter() } + + return NextScript(parameterList, allParametersScript.restOfScript) +} + +private fun ScriptPart.toParameter(): Parameter { + var script: ScriptPart = this.trimSpace() + return Parameter( + direction = script.getParameterMode().apply { script = nextScriptPart }.value, + name = script.getParameterName().trimSpace().apply { script = nextScriptPart }.value.trim(), + type = script.getParameterType().trimSpace().apply { script = nextScriptPart }.value, + default = script.getParameterDefault().trimSpace().apply { script = nextScriptPart }.value, + ) +} +private fun ScriptPart.getParameterMode(): NextScript { + return when { + restOfScript.startsWith("inout ", true) -> NextScript(INOUT, restOfScript.drop("inout ".length)) + restOfScript.startsWith("in ", true) -> NextScript(IN, restOfScript.drop("in ".length)) + restOfScript.startsWith("out ", true) -> NextScript(OUT, restOfScript.drop("out ".length)) + else -> NextScript(IN, restOfScript) + } +} + +@Throws(ParameterNameMalformed::class) +private fun ScriptPart.getParameterName(): NextScript { + try { + return getNextScript { afterBeginBy(" ", "\n") } + } catch (e: ParseException) { + throw ParameterNameMalformed(this, e) + } +} +private class ParameterNameMalformed(val script: ScriptPart, cause: Throwable): + ParseException("Parameter name is malformed", cause) + +@Throws(ParameterTypeMalformed::class) +private fun ScriptPart.getParameterType(): NextScript { + val fullType = try { + val endTextList = arrayOf(" default ", "=", ")") + getNextScript { afterBeginBy(texts = endTextList) } + } catch (e: ParseError) { + throw ParameterTypeMalformed(this, e) + } + + var rest: ScriptPart = fullType.valueAsScriptPart() + + val name = rest + .getNextScript { afterBeginBy("(") } + .apply { rest = nextScriptPart } + val precision = rest + .getNextInteger() + .apply { rest = nextScriptPart } + val scale = rest + .getNextInteger() + .apply { rest = nextScriptPart } + + return NextScript( + ParameterType( + name = name.value.trim(), + precision = precision.value, + scale = scale.value + ), fullType.nextScriptPart.restOfScript + ) +} + +internal class ParameterTypeMalformed(val script: ScriptPart, cause: Throwable): + ParseException("Parameter type is malformed", cause) + +@Throws(ParameterDefaultMalformed::class) +private fun ScriptPart.getParameterDefault(): NextScript { + return if (this.isEmpty() || this.restOfScript == ")") { + NextScript(null, "") + } else { + """^(\s*=\s*|\s+default\s+)(.+)\s*$""" + .toRegex(IGNORE_CASE) + .find(restOfScript) + .let { it ?: throw ParameterDefaultMalformed(this) } + .let { it.groups[2]!!.value } + .let { NextScript(it, "") } + } +} + +private class ParameterDefaultMalformed(val script: ScriptPart): + ParseException("Parameter default is malformed") + +/** + * TODO Finalize this + */ +internal fun ScriptPart.getReturns(): NextScript { + return NextScript(Void(), "") +} + +class ParseError(message: String? = null, cause: Throwable? = null): + ParseException(message ?: "Parsing fail", cause) \ No newline at end of file diff --git a/src/main/kotlin/fr/postgresjson/definition/parse/ParsingHelper.kt b/src/main/kotlin/fr/postgresjson/definition/parse/ParsingHelper.kt new file mode 100644 index 0000000..74d7333 --- /dev/null +++ b/src/main/kotlin/fr/postgresjson/definition/parse/ParsingHelper.kt @@ -0,0 +1,164 @@ +package fr.postgresjson.definition.parse + +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind.EXACTLY_ONCE +import kotlin.contracts.contract + +@JvmInline +internal value class ScriptPart(val restOfScript: String) { + fun copy(block: (String) -> String): ScriptPart { + return ScriptPart(block(restOfScript)) + } + + fun isEmpty() = restOfScript.isEmpty() +} + +internal class NextScript(val value: T, val restOfScript: String) { + val nextScriptPart: ScriptPart = ScriptPart(restOfScript) + fun isLast() = restOfScript == "" + fun isEmptyValue() = value == "" || value == null +} + +internal fun ScriptPart.removeParentheses(): ScriptPart { + return if (restOfScript.take(1) == "(" && restOfScript.takeLast(1) == ")") { + this.copy { + it.drop(1).dropLast(1) + } + } else { + this + } +} +/** + * Get next part of script. + * You can define a list of characters that end the part of script. Like `(` or space. + */ +@Throws(ParseError::class) +internal fun ScriptPart.getNextScript(isEnd: Context.() -> Boolean = { false }): NextScript { + val status = Status() + + fun String.unescape(): String { + val first = take(1) + val last = takeLast(1) + return if (first == last && first in listOf("\"", "'")) { + drop(1).dropLast(1).replace("$first$first", first) + } else { + this + } + } + + for ((index, c) in restOfScript.withIndex()) { + val nextChar = restOfScript.getOrNull(index + 1) + val prevChar = restOfScript.getOrNull(index - 1) + if (c == '"' && (nextChar != '"' && prevChar != '"')) { + status.doubleQuoted = !status.doubleQuoted + } else if (c == '\'' && (nextChar != '\'' && prevChar != '\'')) { + status.simpleQuoted = !status.simpleQuoted + } else if (c == '(' && status.isNotQuoted()) { + status.parentheses++ + } else if (c == ')' && status.isNotQuoted()) { + status.parentheses-- + } else if (c == '[' && status.isNotQuoted()) { + status.brackets++ + } else if (c == ']' && status.isNotQuoted()) { + status.brackets-- + } else if (c == '{' && status.isNotQuoted()) { + status.braces++ + } else if (c == '}' && status.isNotQuoted()) { + status.braces-- + } + + if (isEnd(Context(index, c, status.copy(), restOfScript))) { + return NextScript(restOfScript.take(index + 1).unescape(), restOfScript.drop(index + 1)) + } + } + if (status.isNotEscaped()) { + return NextScript(restOfScript.unescape().trim(), "").trimSpace() + } + throw ParseError() +} + +internal fun NextScript.trimSpace(): NextScript { + val spaces = charArrayOf(' ', '\n', '\t') + return trim(chars = spaces) +} + +internal fun ScriptPart.trimSpace(): ScriptPart { + for ((n, char) in restOfScript.withIndex()) { + if (char !in listOf(' ', '\n', '\t')) { + return ScriptPart( + restOfScript.drop(n) + ) + } + } + return ScriptPart(restOfScript) +} + +internal fun NextScript.trim(vararg chars: Char): NextScript { + return NextScript(value, restOfScript.apply { dropWhile { it in chars } }) +} + +internal fun ScriptPart.trimEnd(vararg chars: Char): ScriptPart { + return this.change { dropLastWhile { it in chars } } +} + +internal fun ScriptPart.split(delimiter: String): List { + val parts: MutableList = mutableListOf() + var rest: ScriptPart = this + do { + rest = rest.trimSpace() + .getNextScript { status.isNotEscaped() && currentChar.toString() == delimiter } + .trimSpace() + .also { parts.add(it.valueAsScriptPart().trimSpace().trimEnd(',')) } + .nextScriptPart + } while (!rest.isEmpty()) + + return parts +} + +/** + * Return the value as ScriptPart + */ +internal fun NextScript.valueAsScriptPart(): ScriptPart = ScriptPart(value) + +@OptIn(ExperimentalContracts::class) +internal inline fun ScriptPart.change(block: String.() -> String): ScriptPart { + contract { + callsInPlace(block, EXACTLY_ONCE) + } + return ScriptPart(restOfScript.run(block)) +} + + +internal fun ScriptPart.getNextInteger(): NextScript { + val trimmed = restOfScript.trimStart { !it.isDigit() } + val digits = trimmed.takeWhile { it.isDigit() } + val restOfScript = trimmed.trimStart { it.isDigit() } + return NextScript(digits.toIntOrNull(), restOfScript).trimSpace() +} + + + +internal data class Status( + var doubleQuoted: Boolean = false, // " + var simpleQuoted: Boolean = false, // ' + var parentheses: Int = 0, // () + var brackets: Int = 0, // [] + var braces: Int = 0, // {} +) { + fun isQuoted(): Boolean = doubleQuoted || simpleQuoted + fun isNotQuoted(): Boolean = !isQuoted() + fun isNotEscaped(): Boolean = isNotQuoted() && parentheses == 0 && brackets == 0 && braces == 0 +} + +internal data class Context( + val index: Int, + val currentChar: Char, + val status: Status, + val script: String, +) { + fun afterBeginBy(vararg texts: String): Boolean = texts.any { + script.substring(index + 1).take(it.length) == it + } + + val nextChar: Char? get() = script.substring(index + 1).getOrNull(0) +} \ No newline at end of file diff --git a/src/main/kotlin/fr/postgresjson/functionGenerator/FunctionGenerator.kt b/src/main/kotlin/fr/postgresjson/functionGenerator/FunctionGenerator.kt index d11eea0..b2a3f6b 100644 --- a/src/main/kotlin/fr/postgresjson/functionGenerator/FunctionGenerator.kt +++ b/src/main/kotlin/fr/postgresjson/functionGenerator/FunctionGenerator.kt @@ -1,8 +1,7 @@ package fr.postgresjson.functionGenerator -import com.github.jasync.sql.db.util.length import fr.postgresjson.definition.Function -import fr.postgresjson.definition.Function.Returns +import fr.postgresjson.definition.Returns import fr.postgresjson.definition.Parameter import fr.postgresjson.definition.Parameter.Direction.IN import fr.postgresjson.definition.Parameter.Direction.INOUT diff --git a/src/main/kotlin/fr/postgresjson/migration/Function.kt b/src/main/kotlin/fr/postgresjson/migration/Function.kt index 1527174..d996ce5 100644 --- a/src/main/kotlin/fr/postgresjson/migration/Function.kt +++ b/src/main/kotlin/fr/postgresjson/migration/Function.kt @@ -3,6 +3,7 @@ package fr.postgresjson.migration import com.github.jasync.sql.db.postgresql.exceptions.GenericDatabaseException import fr.postgresjson.connexion.Connection import fr.postgresjson.connexion.execute +import fr.postgresjson.definition.parse.parseFunction import fr.postgresjson.migration.Migration.Action import fr.postgresjson.migration.Migration.Status import java.util.Date @@ -30,8 +31,8 @@ data class Function( connection: Connection, executedAt: Date? = null ) : this( - DefinitionFunction(up), - DefinitionFunction(down), + parseFunction(up), + parseFunction(down), connection, executedAt ) diff --git a/src/main/kotlin/fr/postgresjson/migration/MigrationExecutor.kt b/src/main/kotlin/fr/postgresjson/migration/MigrationExecutor.kt index 1f6d4bf..86ce475 100644 --- a/src/main/kotlin/fr/postgresjson/migration/MigrationExecutor.kt +++ b/src/main/kotlin/fr/postgresjson/migration/MigrationExecutor.kt @@ -2,6 +2,7 @@ package fr.postgresjson.migration import com.fasterxml.jackson.core.type.TypeReference import fr.postgresjson.connexion.Connection +import fr.postgresjson.definition.parse.parseFunction import fr.postgresjson.migration.Migration.Action import fr.postgresjson.migration.Migration.Status import fr.postgresjson.utils.LoggerDelegate @@ -147,7 +148,7 @@ class MigrationExecutor private constructor( } fun addFunction(sql: String): MigrationExecutor { - addFunction(DefinitionFunction(sql)) + addFunction(parseFunction(sql)) return this } diff --git a/src/test/kotlin/fr/postgresjson/definition/FunctionTest.kt b/src/test/kotlin/fr/postgresjson/definition/FunctionTest.kt index 703f959..f999826 100644 --- a/src/test/kotlin/fr/postgresjson/definition/FunctionTest.kt +++ b/src/test/kotlin/fr/postgresjson/definition/FunctionTest.kt @@ -1,5 +1,6 @@ package fr.postgresjson.definition +import fr.postgresjson.definition.parse.parseFunction import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.shouldBe @@ -7,7 +8,7 @@ import io.kotest.matchers.shouldBe class FunctionTest: FreeSpec({ "Function name" - { "all in lower" { - Function( + parseFunction( // language=PostgreSQL """ create or replace function myfun() returns text language plpgsql as @@ -19,7 +20,7 @@ class FunctionTest: FreeSpec({ } "first letter caps" { - Function( + parseFunction( // language=PostgreSQL """ create or replace function Myfun() returns text language plpgsql as @@ -31,7 +32,7 @@ class FunctionTest: FreeSpec({ } "with numbers" { - Function( + parseFunction( // language=PostgreSQL """ create or replace function myfun001() returns text language plpgsql as @@ -43,7 +44,7 @@ class FunctionTest: FreeSpec({ } "escaped name with space" { - Function( + parseFunction( // language=PostgreSQL """ create or replace function "My fun"() returns text language plpgsql as @@ -55,7 +56,7 @@ class FunctionTest: FreeSpec({ } "escaped name with double quote in name" { - Function( + parseFunction( // language=PostgreSQL """ create or replace function "My""fun" () returns text language plpgsql as @@ -67,7 +68,7 @@ class FunctionTest: FreeSpec({ } "name with new line before and after" { - Function( + parseFunction( // language=PostgreSQL """ create or replace function @@ -85,7 +86,7 @@ class FunctionTest: FreeSpec({ "Parameters" - { "One parameter text" - { - val param = Function( + val param = parseFunction( // language=PostgreSQL """ create or replace function myfun(one text) returns text language plpgsql as @@ -107,7 +108,7 @@ class FunctionTest: FreeSpec({ } "Two parameters" - { - val param = Function( + val param = parseFunction( // language=PostgreSQL """ create or replace function myfun(one text, two int) returns text language plpgsql as @@ -131,7 +132,7 @@ class FunctionTest: FreeSpec({ } "parameters with `character varying(255)`" - { - val param = Function( + val param = parseFunction( // language=PostgreSQL """ create or replace function myfun(one character varying(255)) returns text language plpgsql as @@ -158,7 +159,7 @@ class FunctionTest: FreeSpec({ } "parameters with `numeric(16, 8)`" - { - val param = Function( + val param = parseFunction( // language=PostgreSQL """ create or replace function myfun(one numeric(16, 8)) returns text language plpgsql as @@ -188,7 +189,7 @@ class FunctionTest: FreeSpec({ } "parameters with default text" - { - val param = Function( + val param = parseFunction( // language=PostgreSQL """ create or replace function myfun(one text default 'example') returns text language plpgsql as diff --git a/src/test/kotlin/fr/postgresjson/functionGenerator/FunctionGeneratorTest.kt b/src/test/kotlin/fr/postgresjson/functionGenerator/FunctionGeneratorTest.kt index 8590080..e6f4bf3 100644 --- a/src/test/kotlin/fr/postgresjson/functionGenerator/FunctionGeneratorTest.kt +++ b/src/test/kotlin/fr/postgresjson/functionGenerator/FunctionGeneratorTest.kt @@ -1,6 +1,6 @@ package fr.postgresjson.functionGenerator -import fr.postgresjson.definition.Function +import fr.postgresjson.definition.parse.parseFunction import io.kotest.core.Tag import io.kotest.core.spec.style.StringSpec import org.amshove.kluent.`should be equal to` @@ -34,7 +34,7 @@ class FunctionGeneratorTest : StringSpec({ |} """.trimMargin() - generator.generate(Function(functionSql)) `should be equal to` expectedGenerated + generator.generate(parseFunction(functionSql)) `should be equal to` expectedGenerated } "generate function with return void" { @@ -60,7 +60,7 @@ class FunctionGeneratorTest : StringSpec({ |} """.trimMargin() - generator.generate(Function(functionSql)) `should be equal to` expectedGenerated + generator.generate(parseFunction(functionSql)) `should be equal to` expectedGenerated } "generate function with multiple args and defaults" { @@ -90,6 +90,6 @@ class FunctionGeneratorTest : StringSpec({ |} """.trimMargin() - generator.generate(Function(functionSql)) `should be equal to` expectedGenerated + generator.generate(parseFunction(functionSql)) `should be equal to` expectedGenerated } })