Skip to content

Commit

Permalink
Fixes #52 Allow tix config to be defined as part of body.
Browse files Browse the repository at this point in the history
  • Loading branch information
ncipollo committed Oct 11, 2023
1 parent f336520 commit 5412cae
Show file tree
Hide file tree
Showing 25 changed files with 588 additions and 152 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.transform
import org.tix.config.ConfigurationPaths
import org.tix.config.data.raw.RawTixConfiguration
import org.tix.config.domain.reader.MarkdownConfigurationReader
import org.tix.config.domain.reader.RootConfigurationReader
import org.tix.config.domain.reader.SavedConfigurationReader
import org.tix.config.domain.reader.WorkspaceConfigurationReader
Expand All @@ -15,6 +16,7 @@ import org.tix.domain.FlowTransformer
internal class ConfigurationReadUseCase(
private val configPaths: ConfigurationPaths,
private val reader: RawTixConfigurationReader,
private val markdownConfigurationReader: MarkdownConfigurationReader = MarkdownConfigurationReader(),
private val rootConfigReader: RootConfigurationReader = RootConfigurationReader(configPaths, reader),
private val savedConfigReader: SavedConfigurationReader = SavedConfigurationReader(configPaths, reader),
private val workspaceConfigReader: WorkspaceConfigurationReader = WorkspaceConfigurationReader(configPaths, reader)
Expand All @@ -25,13 +27,19 @@ internal class ConfigurationReadUseCase(
coroutineScope {
val rootDeferred = async { rootConfigReader.readRootConfig(configOptions) }
val workspaceDeferred = async { workspaceConfigReader.readWorkspaceConfig(configOptions) }
val markdownDeferred = async {
markdownConfigurationReader.configFromMarkdown(configOptions.markdownContent)
}
val workspace = workspaceDeferred.await()
val savedDeferred = async { savedConfigReader.readSavedConfigs(configOptions, workspace) }
val markdownConfig = markdownDeferred.await()
val savedDeferred =
async { savedConfigReader.readSavedConfigs(configOptions, markdownConfig, workspace) }

val configs = listOfNotNull(
rootDeferred.await(),
*savedDeferred.await().toTypedArray(),
workspace
workspace,
markdownConfig
)
emit(configs)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import org.tix.platform.path.pathByExpandingTilde
data class ConfigurationSourceOptions(
val workspaceDirectory: String? = null,
val savedConfigName: String? = null,
val includeRootConfig: Boolean = true
val includeRootConfig: Boolean = true,
val markdownContent: String? = null
) {
companion object {
fun forMarkdownSource(markdownPath: String, includeConfigName: String? = null) =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.tix.config.domain.reader

import kotlinx.serialization.SerializationException
import kotlinx.serialization.decodeFromString
import org.tix.config.data.raw.RawTixConfiguration
import org.tix.error.TixError
import org.tix.feature.plan.domain.parse.configparser.ConfigLanguage
import org.tix.serialize.TixSerializers

class MarkdownConfigParser {
private val json = TixSerializers.json()
private val yaml = TixSerializers.yaml()

fun parse(code: String, language: ConfigLanguage) =
when (language) {
ConfigLanguage.JSON -> parseJson(code)
ConfigLanguage.NO_CONFIG -> null
ConfigLanguage.YAML -> parseYaml(code)
}

private fun parseJson(code: String) = rethrowErrorsWithMoreContext {
json.decodeFromString<RawTixConfiguration>(code)
}

private fun parseYaml(code: String) = rethrowErrorsWithMoreContext {
yaml.decodeFromString<RawTixConfiguration>(code)
}

private fun rethrowErrorsWithMoreContext(decodeBlock: () -> RawTixConfiguration) =
try {
decodeBlock()
} catch (ex: SerializationException) {
throw TixError(message = "Failed to parse configuration in markdown file", cause = ex)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.tix.config.domain.reader

import org.intellij.markdown.MarkdownElementTypes
import org.intellij.markdown.ast.ASTNode
import org.intellij.markdown.parser.MarkdownParser
import org.tix.config.data.raw.RawTixConfiguration
import org.tix.feature.plan.domain.parse.configparser.ConfigLanguageDetector
import org.tix.feature.plan.domain.parse.defaultMarkdownParser
import org.tix.feature.plan.domain.parse.nodeparser.CodeFenceSegmentCreator

class MarkdownConfigurationReader(private val markdownParser: MarkdownParser = defaultMarkdownParser()) {
private val configParser = MarkdownConfigParser()
private val headerTypes = listOf(
MarkdownElementTypes.ATX_1.name,
MarkdownElementTypes.ATX_2.name,
MarkdownElementTypes.ATX_3.name,
MarkdownElementTypes.ATX_4.name,
MarkdownElementTypes.ATX_5.name,
MarkdownElementTypes.ATX_6.name
)

fun configFromMarkdown(markdown: String?): RawTixConfiguration? {
if (markdown == null) {
return null
}
val node = markdownTree(markdown)
val preambleNodes = node.children.takeWhile { !headerTypes.contains(it.type.name) }
return preambleNodes.firstCodeFence()
?.let {codeFence ->
val segment = CodeFenceSegmentCreator.createCodeSegment(codeFence, markdown)
val configType = ConfigLanguageDetector.detect(segment)
configParser.parse(segment.code, configType)
}
}

private fun markdownTree(markdown: String) = markdownParser.buildMarkdownTreeFromString(markdown)

private fun List<ASTNode>.firstCodeFence() =
firstOrNull { it.type.name == MarkdownElementTypes.CODE_FENCE.name }
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ internal class SavedConfigurationReader(
) {
fun readSavedConfigs(
configOptions: ConfigurationSourceOptions,
markdownConfig: RawTixConfiguration?,
workspaceConfig: RawTixConfiguration?
) = listOfNotNull(
includedFromWorkspaceConfig(markdownConfig),
includedFromWorkspaceConfig(workspaceConfig),
includedFromConfigOptions(configOptions),
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.tix.feature.plan.domain.parse.configparser

enum class ConfigLanguage {
JSON,
NO_CONFIG,
YAML
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
package org.tix.feature.plan.domain.parse.fieldparser
package org.tix.feature.plan.domain.parse.configparser

import org.tix.ticket.body.CodeBlockSegment

object FieldLanguageDetector {
object ConfigLanguageDetector {
fun detect(block: CodeBlockSegment) =
when(block.language) {
"tix" -> detectByCode(block)
"tix_json" -> FieldLanguage.JSON
"tix_yaml", "tix_yml" -> FieldLanguage.YAML
else -> FieldLanguage.NO_FIELDS
"tix", "tix_config" -> detectByCode(block)
"tix_json", "tix_config_json" -> ConfigLanguage.JSON
"tix_yaml", "tix_yml", "tix_config_yaml", "tix_config_yml" -> ConfigLanguage.YAML
else -> ConfigLanguage.NO_CONFIG
}

private fun detectByCode(codeBlock: CodeBlockSegment) =
if(firstStatement(codeBlock).startsWith("{") ) {
FieldLanguage.JSON
ConfigLanguage.JSON
} else {
FieldLanguage.YAML
ConfigLanguage.YAML
}

private fun firstStatement(codeBlock: CodeBlockSegment) =
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.tix.feature.plan.domain.parse.fieldparser

import org.tix.feature.plan.domain.parse.configparser.ConfigLanguage
import org.tix.serialize.TixSerializers
import org.tix.serialize.decodeDynamicElement
import org.tix.serialize.dynamic.emptyDynamic
Expand All @@ -8,11 +9,11 @@ object FieldParser {
private val json = TixSerializers.json()
private val yaml = TixSerializers.yaml()

fun parse(code: String, language: FieldLanguage) =
fun parse(code: String, language: ConfigLanguage) =
when (language) {
FieldLanguage.JSON -> parseJson(code)
FieldLanguage.NO_FIELDS -> emptyDynamic()
FieldLanguage.YAML -> parseYaml(code)
ConfigLanguage.JSON -> parseJson(code)
ConfigLanguage.NO_CONFIG -> emptyDynamic()
ConfigLanguage.YAML -> parseYaml(code)
}

private fun parseJson(code: String) = json.decodeDynamicElement(code)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
package org.tix.feature.plan.domain.parse.nodeparser

import kotlinx.serialization.SerializationException
import org.intellij.markdown.MarkdownTokenTypes
import org.intellij.markdown.ast.ASTNode
import org.intellij.markdown.ast.findChildOfType
import org.intellij.markdown.ast.getTextInNode
import org.tix.feature.plan.domain.parse.fieldparser.FieldLanguage
import org.tix.feature.plan.domain.parse.fieldparser.FieldLanguageDetector
import org.tix.feature.plan.domain.parse.configparser.ConfigLanguage
import org.tix.feature.plan.domain.parse.configparser.ConfigLanguageDetector
import org.tix.feature.plan.domain.parse.fieldparser.FieldParser
import org.tix.feature.plan.domain.parse.parseError
import org.tix.ticket.body.CodeBlockSegment

internal class CodeFenceParser: NodeParser {
override fun parse(arguments: ParserArguments): ParserResult {
val segment = CodeBlockSegment(code = code(arguments), language = lang(arguments))
val segment = CodeFenceSegmentCreator.createCodeSegment(arguments.currentNode, arguments.markdownText)

val fieldLanguage = FieldLanguageDetector.detect(segment)
if (fieldLanguage == FieldLanguage.NO_FIELDS) {
val fieldLanguage = ConfigLanguageDetector.detect(segment)
if (fieldLanguage == ConfigLanguage.NO_CONFIG) {
arguments.state.addBodySegments(segment)
} else {
try {
Expand All @@ -29,21 +24,4 @@ internal class CodeFenceParser: NodeParser {

return arguments.resultsFromArgs()
}

private fun code(arguments: ParserArguments) =
arguments.currentNode.children
.joinToString(separator = "") { mapChildNode(it, arguments) }

private fun mapChildNode(node: ASTNode, arguments: ParserArguments) =
when(node.type.name) {
MarkdownTokenTypes.CODE_FENCE_CONTENT.name -> node.getTextInNode(arguments.markdownText).toString()
MarkdownTokenTypes.EOL.name -> "\n"
else -> ""
}

private fun lang(arguments: ParserArguments) =
arguments.currentNode
.findChildOfType(MarkdownTokenTypes.FENCE_LANG)
?.getTextInNode(arguments.markdownText)
?.toString() ?: ""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.tix.feature.plan.domain.parse.nodeparser

import org.intellij.markdown.MarkdownTokenTypes
import org.intellij.markdown.ast.ASTNode
import org.intellij.markdown.ast.findChildOfType
import org.intellij.markdown.ast.getTextInNode
import org.tix.ticket.body.CodeBlockSegment

object CodeFenceSegmentCreator {
fun createCodeSegment(node: ASTNode, markdownText: String) =
CodeBlockSegment(code = code(node, markdownText), language = lang(node, markdownText))

private fun code(node: ASTNode, markdownText: String) =
node.children
.joinToString(separator = "") { mapChildNode(it, markdownText) }

private fun mapChildNode(node: ASTNode, markdownText: String) =
when (node.type.name) {
MarkdownTokenTypes.CODE_FENCE_CONTENT.name -> node.getTextInNode(markdownText).toString()
MarkdownTokenTypes.EOL.name -> "\n"
else -> ""
}

private fun lang(node: ASTNode, markdownText: String) =
node
.findChildOfType(MarkdownTokenTypes.FENCE_LANG)
?.getTextInNode(markdownText)
?.toString() ?: ""
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package org.tix.feature.plan.domain.parse.state

import org.tix.serialize.dynamic.DynamicElement
import org.tix.ticket.body.BodySegment
import org.tix.ticket.body.LinebreakSegment
import org.tix.serialize.dynamic.DynamicElement

internal class ParserState {
private val ticketPath: MutableList<PartialTicket> = ArrayList()
Expand Down Expand Up @@ -41,5 +41,5 @@ internal class ParserState {
}

fun buildNestedBody(buildBlock: () -> Unit) : List<BodySegment> =
currentTicket?.buildNestedBody(buildBlock) ?: error("no ticket has been started")
currentTicket?.buildNestedBody(buildBlock) ?: emptyList()
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ class PlanSourceCombiner(
) : FlowTransformer<MarkdownPlanAction, PlanSourceResult> {
override fun transformFlow(upstream: Flow<MarkdownPlanAction>): Flow<PlanSourceResult> =
upstream.flatMapLatest { action ->
flowOf(action.configSourceOptions)
.transform(configurationUseCase)
.combine(markdown(action.markdownSource)) { configResult, markdownResult ->
toResult(configResult, markdownResult)
markdown(action.markdownSource)
.flatMapLatest { markdownResult ->
val markdownContent = markdownResult.getOrNull()
val optionsWithMarkdown = action.configSourceOptions.copy(markdownContent = markdownContent)
flowOf(optionsWithMarkdown)
.transform(configurationUseCase)
.map { configResult -> toResult(configResult, markdownResult) }
}.catch {
// Catch parsing exceptions from config or markdown
emit(PlanSourceResult.Error(it.toTixError()))
Expand Down
Loading

0 comments on commit 5412cae

Please sign in to comment.