Files
TheTree_mock/scripts/core/state_transaction.gd
T

267 lines
7.2 KiB
GDScript

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