Skip to content

Commit

Permalink
Use delegates instead of recursive computes when generating args
Browse files Browse the repository at this point in the history
  • Loading branch information
hubvd committed Sep 13, 2024
1 parent f1778f8 commit 799bceb
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 286 deletions.
1 change: 0 additions & 1 deletion odoo/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ dependencies {
implementation(libs.process)
implementation(libs.xmlbuilder)
implementation(libs.kodein.di)
implementation(libs.kotlin.reflect)
implementation(libs.serialization.json)

testImplementation(testFixtures(project(":workspace")))
Expand Down
105 changes: 105 additions & 0 deletions odoo/src/CliGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.github.hubvd.odootools.odoo

import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

class OptionCompute(
val name: String,
initialValue: String?,
private val block: (() -> String?)? = null,
) : ReadOnlyProperty<CliGenerator, String?> {
private var value = initialValue
private var hasBeenCalled = false

fun get(): String? {
if (value == null && !hasBeenCalled && block != null) {
value = block()
hasBeenCalled = true
}
return value
}

override fun getValue(thisRef: CliGenerator, property: KProperty<*>) = get()
}

class EnvCompute(
val name: String,
private val block: (() -> String?)? = null,
) : ReadOnlyProperty<CliGenerator, String?> {
private var value: String? = null
private var hasBeenCalled = false

fun get(): String? {
if (!hasBeenCalled && block != null) {
value = block()
hasBeenCalled = true
}
return value
}

override fun getValue(thisRef: CliGenerator, property: KProperty<*>) = get()
}

class FlagCompute(
val name: String,
initialValue: Boolean,
private val block: (() -> Boolean)? = null,
) : ReadOnlyProperty<CliGenerator, Boolean> {
private var value = initialValue
private var hasBeenCalled = false

fun get(): Boolean {
if (!value && !hasBeenCalled && block != null) {
value = block()
hasBeenCalled = true
}
return value
}

override fun getValue(thisRef: CliGenerator, property: KProperty<*>) = get()
}

abstract class CliGenerator(
options: Map<String, String>,
flags: Set<String>,
) {

private val options: MutableMap<String, String> = options.toMap(HashMap())
private val flags = flags.toHashSet()
private val camelCaseRe = Regex("([a-z0-9])([A-Z])")

val registeredOptions = ArrayList<OptionCompute>()
val registeredFlags = ArrayList<FlagCompute>()
val registeredEnv = ArrayList<EnvCompute>()
val registeredEffects = ArrayList<() -> Unit>()

fun effect(block: () -> Unit) {
registeredEffects += block
}

fun option(
block: (() -> String?)? = null,
): PropertyDelegateProvider<CliGenerator, ReadOnlyProperty<CliGenerator, String?>> =
PropertyDelegateProvider { thisRef, property ->
val name = camelCaseRe.replace(property.name, "\$1-\$2").lowercase()
OptionCompute(name, options[name], block).also { registeredOptions += it }
}

fun flag(
block: (() -> Boolean)? = null,
): PropertyDelegateProvider<CliGenerator, ReadOnlyProperty<CliGenerator, Boolean>> =
PropertyDelegateProvider { thisRef, property ->
val name = camelCaseRe.replace(property.name, "\$1-\$2").lowercase()
FlagCompute(name, flags.contains(name), block).also { registeredFlags += it }
}

fun env(
name: String? = null,
block: () -> String?,
): PropertyDelegateProvider<CliGenerator, ReadOnlyProperty<CliGenerator, String?>> =
PropertyDelegateProvider { thisRef, property ->
val name = name ?: camelCaseRe.replace(property.name, "\$1_\$2").uppercase()
EnvCompute(name, block).also { registeredEnv += it }
}
}
137 changes: 21 additions & 116 deletions odoo/src/ContextGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ import com.github.ajalt.clikt.parameters.options.OptionWithValues
import com.github.hubvd.odootools.odoo.commands.*
import com.github.hubvd.odootools.workspace.Workspace
import com.github.hubvd.odootools.workspace.WorkspaceConfig
import java.lang.reflect.Proxy
import java.nio.file.Path
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.jvm.javaGetter
import kotlin.reflect.typeOf

private typealias StringOption = OptionWithValues<String?, String, String>
private typealias Flag = OptionWithValues<Boolean, Boolean, Boolean>
Expand All @@ -18,15 +14,10 @@ data class RunConfiguration(
val args: List<String>,
val env: Map<String, String>,
val cwd: Path,
val context: DslContext,
val effects: List<(DslContext) -> Unit>,
val odoo: Odoo,
val effects: List<() -> Unit>,
)

@DslMarker
annotation class CmdComputeDsl

typealias Effect = (DslContext) -> Unit

class ContextGenerator(
private val options: List<Option>,
private val arguments: List<String>,
Expand All @@ -35,53 +26,7 @@ class ContextGenerator(
private val config: WorkspaceConfig,
) {

private val graph = HashMap<String, List<String>>()
private val computes = HashMap<String, (MutableDslContext) -> Unit>()

private val effects = ArrayList<Effect>()
private val envs = HashMap<String, (MutableDslContext) -> String?>()

data class Depends(val depends: List<String>)

@CmdComputeDsl
fun effect(block: DslContext.() -> Unit) {
effects += block
}

@CmdComputeDsl
fun depends(vararg tmpl: String, block: Depends.() -> Unit) {
block(Depends(tmpl.toList()))
}

@CmdComputeDsl
fun flag(name: String, block: DslContext.() -> Boolean) {
computes[name] = { if (block(it)) it.flags += name }
}

@CmdComputeDsl
fun option(name: String, block: DslContext.() -> String?) {
computes[name] = { cmd -> block(cmd)?.let { cmd.options[name] = it } }
}

@CmdComputeDsl
fun Depends.flag(name: String, block: DslContext.() -> Boolean) {
graph[name] = depends
computes[name] = { if (block(it)) it.flags += name }
}

@CmdComputeDsl
fun Depends.option(name: String, block: DslContext.() -> String?) {
graph[name] = depends
computes[name] = { cmd -> block(cmd)?.let { cmd.options[name] = it } }
}

@CmdComputeDsl
fun env(name: String, block: DslContext.() -> String?) {
envs[name] = block
}

fun generate(generator: ContextGenerator.() -> Unit): RunConfiguration {
generator()
fun generate(): RunConfiguration {
val flags = HashSet<String>()
val options = HashMap<String, String>()
val env = HashMap<String, String>()
Expand All @@ -106,75 +51,35 @@ class ContextGenerator(
}
}

val camelCaseRe = Regex("([a-z])([A-Z])")

val proxy = Proxy.newProxyInstance(
this.javaClass.classLoader,
arrayOf(OdooOptions::class.java),
) { _, method, _ ->
val getter = method.declaringClass.kotlin.declaredMemberProperties
.find { it.javaGetter == method }
?: error("Unsupported method ${method.name}")

val optionName = camelCaseRe.replace(getter.name, "\$1-\$2").lowercase()

when (getter.returnType) {
typeOf<String?>() -> options[optionName]
val odoo = Odoo(workspace, config, options, flags)

typeOf<Boolean>() -> flags.contains(optionName)
odoo.registeredFlags
.filter { it.get() }
.forEach { flags.add(it.name) }

else -> error("Unsupported property type")
}
} as OdooOptions
odoo.registeredOptions
.mapNotNull { it.get()?.let { value -> it.name to value } }
.forEach { options[it.first] = it.second }

if (proxy.restart) {
options.keys.removeAll(ignoreIfRestart)
flags.removeAll(ignoreIfRestart)
if (odoo.restart) {
ignoreIfRestart.forEach { options.remove(it) }
}

val context = MutableDslContext(
workspace = workspace,
odooOptions = proxy,
flags = flags,
options = options,
env = env,
config = config,
)

val queue = computes.keys.toHashSet()
queue.removeAll(flags)
queue.removeAll(options.keys)

if (context.restart) {
queue.removeAll(ignoreIfRestart)
}
odoo.registeredEnv
.mapNotNull { it.get()?.let { value -> it.name to value } }
.forEach { env[it.first] = it.second }

val filteredEffects: List<Effect> = if (context.dryRun || context.restart) {
val filteredEffects: List<() -> Unit> = if (odoo.dryRun || odoo.restart) {
emptyList()
} else {
effects
odoo.registeredEffects
}

// TODO: topological sort
while (queue.isNotEmpty()) {
val iter = queue.iterator()
while (iter.hasNext()) {
val compute = iter.next()
val dependencies = graph[compute] ?: emptyList()
if (dependencies.none { it in queue }) {
computes[compute]!!(context)
iter.remove()
}
}
}

envs.forEach { (name, func) -> func(context)?.let { context.env[name] = it } }

val args = buildList(arguments.size + context.flags.size + context.options.size) {
val args = buildList(arguments.size + flags.size + options.size) {
val args = arguments.toMutableList()
if (args.remove("shell")) add("shell")
addAll(args)
context.flags
flags
.filter { it !in ignores }
.forEach {
if (it == "odoo-help") {
Expand All @@ -183,11 +88,11 @@ class ContextGenerator(
add("--$it")
}
}
context.options
options
.filter { it.key !in ignores }
.forEach { add("--${it.key}=${it.value}") }
}

return RunConfiguration(args, context.env, workspace.path, context, filteredEffects)
return RunConfiguration(args, env, workspace.path, odoo, filteredEffects)
}
}
Loading

0 comments on commit 799bceb

Please sign in to comment.