Preserve approved pixel tower prototype as development baseline
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
class_name StateTransaction
|
||||
extends RefCounted
|
||||
|
||||
enum Status {
|
||||
OPEN,
|
||||
COMMITTED,
|
||||
ROLLED_BACK,
|
||||
FAILED,
|
||||
}
|
||||
|
||||
class StagedChange:
|
||||
extends RefCounted
|
||||
|
||||
var label := ""
|
||||
var validate_action := Callable()
|
||||
var apply_action := Callable()
|
||||
var rollback_action := Callable()
|
||||
|
||||
func _init(
|
||||
p_label: String,
|
||||
p_validate_action: Callable,
|
||||
p_apply_action: Callable,
|
||||
p_rollback_action: Callable
|
||||
) -> void:
|
||||
label = p_label
|
||||
validate_action = p_validate_action
|
||||
apply_action = p_apply_action
|
||||
rollback_action = p_rollback_action
|
||||
|
||||
|
||||
var status: Status = Status.OPEN
|
||||
var _changes: Array[StagedChange] = []
|
||||
var _validators: Array[Callable] = []
|
||||
var _configuration_errors: Array[String] = []
|
||||
|
||||
|
||||
func stage_dictionary_set(
|
||||
target: Dictionary,
|
||||
key: Variant,
|
||||
value: Variant,
|
||||
label: String = ""
|
||||
) -> bool:
|
||||
if not _require_open("stage_dictionary_set"):
|
||||
return false
|
||||
|
||||
var had_key := target.has(key)
|
||||
var original_value: Variant = target.get(key)
|
||||
var original_snapshot: Variant = _copy_value(original_value)
|
||||
var staged_value: Variant = _copy_value(value)
|
||||
var change_label := label if not label.is_empty() else "set dictionary key %s" % str(key)
|
||||
|
||||
return stage_custom(
|
||||
change_label,
|
||||
func() -> Variant:
|
||||
if target.has(key) != had_key:
|
||||
return "Dictionary key presence changed before commit."
|
||||
if had_key and target[key] != original_snapshot:
|
||||
return "Dictionary value changed before commit."
|
||||
return true,
|
||||
func() -> Variant:
|
||||
target[key] = _copy_value(staged_value)
|
||||
return true,
|
||||
func() -> void:
|
||||
if had_key:
|
||||
target[key] = original_value
|
||||
else:
|
||||
target.erase(key)
|
||||
)
|
||||
|
||||
|
||||
func stage_dictionary_erase(
|
||||
target: Dictionary,
|
||||
key: Variant,
|
||||
label: String = ""
|
||||
) -> bool:
|
||||
if not _require_open("stage_dictionary_erase"):
|
||||
return false
|
||||
|
||||
var had_key := target.has(key)
|
||||
var original_value: Variant = target.get(key)
|
||||
var original_snapshot: Variant = _copy_value(original_value)
|
||||
var change_label := label if not label.is_empty() else "erase dictionary key %s" % str(key)
|
||||
|
||||
return stage_custom(
|
||||
change_label,
|
||||
func() -> Variant:
|
||||
if target.has(key) != had_key:
|
||||
return "Dictionary key presence changed before commit."
|
||||
if had_key and target[key] != original_snapshot:
|
||||
return "Dictionary value changed before commit."
|
||||
return true,
|
||||
func() -> Variant:
|
||||
target.erase(key)
|
||||
return true,
|
||||
func() -> void:
|
||||
if had_key:
|
||||
target[key] = original_value
|
||||
)
|
||||
|
||||
|
||||
func stage_dictionary_increment(
|
||||
target: Dictionary,
|
||||
key: Variant,
|
||||
delta: Variant,
|
||||
minimum: Variant = null,
|
||||
maximum: Variant = null,
|
||||
label: String = ""
|
||||
) -> bool:
|
||||
if not _require_open("stage_dictionary_increment"):
|
||||
return false
|
||||
if not target.has(key) or not _is_number(target[key]) or not _is_number(delta):
|
||||
_configuration_errors.append("Increment requires an existing numeric value and numeric delta.")
|
||||
return false
|
||||
|
||||
var next_value: Variant = target[key] + delta
|
||||
if minimum != null and (not _is_number(minimum) or next_value < minimum):
|
||||
_configuration_errors.append("Increment result is below its minimum.")
|
||||
return false
|
||||
if maximum != null and (not _is_number(maximum) or next_value > maximum):
|
||||
_configuration_errors.append("Increment result is above its maximum.")
|
||||
return false
|
||||
return stage_dictionary_set(target, key, next_value, label)
|
||||
|
||||
|
||||
func add_validator(validator: Callable) -> bool:
|
||||
if not _require_open("add_validator"):
|
||||
return false
|
||||
if not validator.is_valid():
|
||||
_configuration_errors.append("Validator is not callable.")
|
||||
return false
|
||||
_validators.append(validator)
|
||||
return true
|
||||
|
||||
|
||||
func stage_custom(
|
||||
label: String,
|
||||
validate_action: Callable,
|
||||
apply_action: Callable,
|
||||
rollback_action: Callable
|
||||
) -> bool:
|
||||
if not _require_open("stage_custom"):
|
||||
return false
|
||||
if not apply_action.is_valid() or not rollback_action.is_valid():
|
||||
_configuration_errors.append("Custom change '%s' requires apply and rollback actions." % label)
|
||||
return false
|
||||
_changes.append(StagedChange.new(label, validate_action, apply_action, rollback_action))
|
||||
return true
|
||||
|
||||
|
||||
func validate() -> CommandResult:
|
||||
if status != Status.OPEN:
|
||||
return CommandResult.failed(
|
||||
CommandResult.Code.TRANSACTION_CLOSED,
|
||||
0,
|
||||
"Transaction is not open."
|
||||
)
|
||||
if not _configuration_errors.is_empty():
|
||||
return CommandResult.failed(
|
||||
CommandResult.Code.VALIDATION_FAILED,
|
||||
0,
|
||||
_configuration_errors[0],
|
||||
{"errors": _configuration_errors.duplicate()}
|
||||
)
|
||||
|
||||
for index in _validators.size():
|
||||
var validator_result := _validation_result(_validators[index].call(), "validator %d" % index)
|
||||
if validator_result != null:
|
||||
return validator_result
|
||||
|
||||
for change in _changes:
|
||||
if not change.validate_action.is_valid():
|
||||
continue
|
||||
var change_result := _validation_result(change.validate_action.call(), change.label)
|
||||
if change_result != null:
|
||||
return change_result
|
||||
return CommandResult.succeeded()
|
||||
|
||||
|
||||
func commit() -> CommandResult:
|
||||
var validation_result := validate()
|
||||
if not validation_result.is_success():
|
||||
if status == Status.OPEN:
|
||||
status = Status.FAILED
|
||||
return validation_result
|
||||
|
||||
var applied: Array[StagedChange] = []
|
||||
for change in _changes:
|
||||
var returned: Variant = change.apply_action.call()
|
||||
var apply_failure := _apply_result(returned, change.label)
|
||||
if apply_failure != null:
|
||||
change.rollback_action.call()
|
||||
for index in range(applied.size() - 1, -1, -1):
|
||||
applied[index].rollback_action.call()
|
||||
status = Status.ROLLED_BACK
|
||||
apply_failure.details["rolled_back"] = true
|
||||
return apply_failure
|
||||
applied.append(change)
|
||||
|
||||
status = Status.COMMITTED
|
||||
return CommandResult.succeeded(0, {"change_count": _changes.size()})
|
||||
|
||||
|
||||
func rollback() -> CommandResult:
|
||||
if status != Status.OPEN:
|
||||
return CommandResult.failed(
|
||||
CommandResult.Code.TRANSACTION_CLOSED,
|
||||
0,
|
||||
"Only an open transaction can be cancelled."
|
||||
)
|
||||
status = Status.ROLLED_BACK
|
||||
return CommandResult.succeeded()
|
||||
|
||||
|
||||
func change_count() -> int:
|
||||
return _changes.size()
|
||||
|
||||
|
||||
func _require_open(operation: String) -> bool:
|
||||
if status == Status.OPEN:
|
||||
return true
|
||||
_configuration_errors.append("%s cannot run on a closed transaction." % operation)
|
||||
return false
|
||||
|
||||
|
||||
func _validation_result(returned: Variant, label: String) -> CommandResult:
|
||||
if returned == null or (returned is bool and returned):
|
||||
return null
|
||||
if returned is CommandResult:
|
||||
var command_result: CommandResult = returned
|
||||
if command_result.is_success():
|
||||
return null
|
||||
return command_result.copy()
|
||||
var message := str(returned) if returned is String and not returned.is_empty() else "Validation failed."
|
||||
return CommandResult.failed(
|
||||
CommandResult.Code.VALIDATION_FAILED,
|
||||
0,
|
||||
"%s: %s" % [label, message],
|
||||
{"change": label}
|
||||
)
|
||||
|
||||
|
||||
func _apply_result(returned: Variant, label: String) -> CommandResult:
|
||||
if returned == null or (returned is bool and returned):
|
||||
return null
|
||||
if returned is CommandResult:
|
||||
var command_result: CommandResult = returned
|
||||
if command_result.is_success():
|
||||
return null
|
||||
return command_result.copy()
|
||||
var message := str(returned) if returned is String and not returned.is_empty() else "Apply action failed."
|
||||
return CommandResult.failed(
|
||||
CommandResult.Code.TRANSACTION_APPLY_FAILED,
|
||||
0,
|
||||
"%s: %s" % [label, message],
|
||||
{"change": label}
|
||||
)
|
||||
|
||||
|
||||
static func _copy_value(value: Variant) -> Variant:
|
||||
if value is Dictionary or value is Array:
|
||||
return value.duplicate(true)
|
||||
return value
|
||||
|
||||
|
||||
static func _is_number(value: Variant) -> bool:
|
||||
return value is int or value is float
|
||||
Reference in New Issue
Block a user