///|
/// Exact prerelease Workbench terminal evidence, separate from frozen driver v1.
#derive(Eq, Debug)
pub(all) enum TerminalOutcome {
Returned(Value)
ModelError(String)
Failed(OutcomeFailure)
Cancelled
}
///|
pub extend TerminalOutcome with Eq::{equal, not_equal}
///|
pub extend TerminalOutcome with @debug.Debug::{to_repr}
///|
#derive(Eq, Debug)
pub(all) enum OutcomeStage {
Acquire
Build
Invoke
Evaluate
Compare
Cleanup
}
///|
pub extend OutcomeStage with Eq::{equal, not_equal}
///|
pub extend OutcomeStage with @debug.Debug::{to_repr}
///|
#derive(Eq, Debug)
pub(all) struct OutcomeFailure {
stage : OutcomeStage
code : String
message : String
}
///|
pub extend OutcomeFailure with Eq::{equal, not_equal}
///|
pub extend OutcomeFailure with @debug.Debug::{to_repr}
///|
#derive(Eq, Debug)
pub(all) struct CallOutcome {
id : String
outcome : TerminalOutcome
}
///|
pub extend CallOutcome with Eq::{equal, not_equal}
///|
pub extend CallOutcome with @debug.Debug::{to_repr}
///|
#derive(Eq, Debug)
pub(all) struct Outcomes {
calls : Array[CallOutcome]
cleanup : OutcomeFailure?
extensions : Bytes?
}
///|
pub extend Outcomes with Eq::{equal, not_equal}
///|
pub extend Outcomes with @debug.Debug::{to_repr}
///|
fn OutcomeStage::text(self : OutcomeStage) -> String {
match self {
Acquire => "acquire"
Build => "build"
Invoke => "invoke"
Evaluate => "evaluate"
Compare => "compare"
Cleanup => "cleanup"
}
}
///|
fn outcome_stage(value : @ion.IonValue) -> OutcomeStage raise {
match string(value) {
"acquire" => Acquire
"build" => Build
"invoke" => Invoke
"evaluate" => Evaluate
"compare" => Compare
"cleanup" => Cleanup
_ => raise Invalid("workbench.invalid_failure")
}
}
///|
fn OutcomeFailure::validate(
self : OutcomeFailure,
cleanup : Bool,
) -> Unit raise {
if (self.stage is Cleanup) != cleanup ||
self.code.length() == 0 ||
self.code.length() > 128 ||
self.message.length() > 512 ||
(
!cleanup &&
(valid_model_error(self.code) || self.code == "execution.cancelled")
) {
raise Invalid("workbench.invalid_failure")
}
}
///|
fn OutcomeFailure::to_json(self : OutcomeFailure) -> Json {
{ "stage": self.stage.text(), "code": self.code, "message": self.message }
}
///|
fn OutcomeFailure::to_ion(self : OutcomeFailure) -> @ion.IonValue {
record([
("stage", @ion.IonValue::string(self.stage.text())),
("code", @ion.IonValue::string(self.code)),
("message", @ion.IonValue::string(self.message)),
])
}
///|
fn outcome_fields(
value : @ion.IonValue,
names : Array[String],
code : String,
) -> Map[String, @ion.IonValue] raise {
if value.has_annotations() {
raise Invalid(code)
}
let f = fields(value, names) catch { _ => raise Invalid(code) }
if f.length() != names.length() {
raise Invalid(code)
}
f
}
///|
fn outcome_failure(
value : @ion.IonValue,
cleanup : Bool,
) -> OutcomeFailure raise {
let code = "workbench.invalid_failure"
let f = outcome_fields(value, ["stage", "code", "message"], code)
let failure = OutcomeFailure::{
stage: outcome_stage(required(f, "stage")),
code: string(required(f, "code")),
message: string(required(f, "message")),
} catch {
_ => raise Invalid(code)
}
failure.validate(cleanup)
failure
}
///|
pub fn TerminalOutcome::to_json(self : TerminalOutcome) -> Json {
match self {
Returned(value) => { "status": "returned", "value": value.to_json() }
ModelError(code) => { "status": "model-error", "code": code }
Failed(failure) => { "status": "failed", "failure": failure.to_json() }
Cancelled => { "status": "cancelled" }
}
}
///|
fn TerminalOutcome::to_ion(self : TerminalOutcome) -> @ion.IonValue {
match self {
Returned(value) =>
record([
("status", @ion.IonValue::string("returned")),
("value", value.to_ion()),
])
ModelError(code) =>
record([
("status", @ion.IonValue::string("model-error")),
("code", @ion.IonValue::string(code)),
])
Failed(failure) =>
record([
("status", @ion.IonValue::string("failed")),
("failure", failure.to_ion()),
])
Cancelled => record([("status", @ion.IonValue::string("cancelled"))])
}
}
///|
fn terminal_outcome(
value : @ion.IonValue,
json : Bool,
) -> TerminalOutcome raise {
let code = "workbench.invalid_outcome"
let f = fields(value, ["status", "value", "code", "failure"]) catch {
_ => raise Invalid(code)
}
let status = string(required(f, "status")) catch { _ => raise Invalid(code) }
let names = match status {
"returned" => ["status", "value"]
"model-error" => ["status", "code"]
"failed" => ["status", "failure"]
"cancelled" => ["status"]
_ => raise Invalid(code)
}
let f = outcome_fields(value, names, code)
match status {
"returned" => {
let v = required(f, "value")
let value = if json { payload_value(v, 0) } else { Value::from_ion(v) }
if value is ModelError(_) {
raise Invalid(code)
}
Returned(value)
}
"model-error" => {
let model_code = string(required(f, "code")) catch {
_ => raise Invalid("execution.model_error_code")
}
if !valid_model_error(model_code) {
raise Invalid("execution.model_error_code")
}
ModelError(model_code)
}
"failed" => Failed(outcome_failure(required(f, "failure"), false))
"cancelled" => Cancelled
_ => abort("checked terminal status")
}
}
///|
pub fn TerminalOutcome::validate(self : TerminalOutcome) -> Unit raise {
match self {
Returned(value) => {
if value is ModelError(_) {
raise Invalid("workbench.invalid_outcome")
}
validate_value(value, 0, Ref(100000))
}
ModelError(code) =>
if !valid_model_error(code) {
raise Invalid("execution.model_error_code")
}
Failed(failure) => failure.validate(false)
Cancelled => ()
}
}
///|
/// Decode the existing terminal shape without fabricating a nonempty call list.
pub fn TerminalOutcome::from_json(value : Json) -> TerminalOutcome raise {
let outcome = terminal_outcome(
@text.read_ion(value.stringify()[:], fuel=Limited(@ion.Budget(128))),
true,
)
outcome.validate()
outcome
}
///|
pub fn Outcomes::validate(self : Outcomes) -> Unit raise {
if self.calls.is_empty() || self.calls.length() > 1000 {
raise Invalid("execution.result_count")
}
let seen : Map[String, Bool] = Map([])
for call in self.calls {
if call.id == "" || call.id.length() > 128 || seen.contains(call.id) {
raise Invalid("execution.call_identity")
}
seen[call.id] = true
call.outcome.validate()
}
if self.cleanup is Some(failure) {
failure.validate(true)
}
if self.extensions is Some(bytes) {
if bytes.length() > 65536 {
raise Invalid("execution.extension_limit")
}
ignore(@binary.read_binary_value(bytes[:], fuel=Limited(@ion.Budget(128))))
}
}
///|
pub fn Outcomes::to_json(self : Outcomes) -> Json {
{
"contractVersion": "0.1.0-draft.5",
"calls": self.calls.map(call => {
({ "id": call.id, "outcome": call.outcome.to_json() } : Json)
}),
"cleanup": self.cleanup.map(OutcomeFailure::to_json).unwrap_or(Json::null()),
"extensions": match self.extensions {
Some(bytes) =>
{
"format": "ion-binary",
"bytes": bytes.to_array().map(b => b.to_int()),
}
None => Json::null()
},
}
}
///|
/// Ion keeps exact native returned values and opaque extension bytes in a blob.
pub fn Outcomes::to_ion(self : Outcomes) -> @ion.IonValue raise {
self.validate()
record([
("contractVersion", @ion.IonValue::string("0.1.0-draft.5")),
(
"calls",
@ion.IonValue::list(
self.calls.map(call => {
record([
("id", @ion.IonValue::string(call.id)),
("outcome", call.outcome.to_ion()),
])
}),
),
),
(
"cleanup",
self.cleanup
.map(OutcomeFailure::to_ion)
.unwrap_or(@ion.IonValue::untyped_null()),
),
(
"extensions",
self.extensions
.map(@ion.IonValue::blob)
.unwrap_or(@ion.IonValue::untyped_null()),
),
])
}
///|
fn absent_outcome_field(value : @ion.IonValue) -> Bool {
!value.has_annotations() && value.kind() is Null(@ion.Null)
}
///|
fn read_outcomes(value : @ion.IonValue, json : Bool) -> Outcomes raise {
let f = outcome_fields(
value,
["contractVersion", "calls", "cleanup", "extensions"],
"workbench.invalid_outcomes",
)
if string(required(f, "contractVersion")) != "0.1.0-draft.5" {
raise Invalid("workbench.invalid_outcomes")
}
let calls = list(required(f, "calls"))
if calls.is_empty() || calls.length() > 1000 {
raise Invalid("execution.result_count")
}
let cleanup = required(f, "cleanup")
let extensions = required(f, "extensions")
let result = Outcomes::{
calls: calls.map(call => {
let f = outcome_fields(
call,
["id", "outcome"],
"workbench.invalid_outcomes",
)
CallOutcome::{
id: string(required(f, "id")),
outcome: terminal_outcome(required(f, "outcome"), json),
}
}),
cleanup: if absent_outcome_field(cleanup) {
None
} else {
Some(outcome_failure(cleanup, true))
},
extensions: if absent_outcome_field(extensions) {
None
} else if json {
Some(json_extension(extensions))
} else {
if extensions.has_annotations() {
raise Invalid("execution.unexpected_annotation")
}
match extensions.kind() {
Blob(bytes) => Some(bytes)
_ => raise Invalid("execution.extension_format")
}
},
}
result.validate()
result
}
///|
pub fn Outcomes::from_json(value : Json) -> Outcomes raise {
read_outcomes(
@text.read_ion(value.stringify()[:], fuel=Limited(@ion.Budget(128))),
true,
)
}
///|
pub fn Outcomes::from_ion(value : @ion.IonValue) -> Outcomes raise {
read_outcomes(value, false)
}
///|
pub fn Outcomes::decode(bytes : Bytes, format : String) -> Outcomes raise {
if bytes.length() > 1048576 {
raise Invalid("execution.input_limit")
}
let source = @utf8.decode(bytes[:])
match format {
"json" => {
ignore(@json.parse(source))
read_outcomes(
@text.read_ion(source[:], fuel=Limited(@ion.Budget(128))),
true,
)
}
"ion-text" =>
read_outcomes(
@text.read_ion(source[:], fuel=Limited(@ion.Budget(128))),
false,
)
_ => raise Invalid("execution.unsupported_format")
}
}
///|
/// Admission succeeds for well-formed failed/cancelled evidence too. It neither
/// runs nor authenticates a execution. Caller admits declarations before this function.
pub fn validate_outcomes(
outcomes : Outcomes,
suite : Suite,
entries : Array[PublicEntry],
definitions : Array[CustomType],
) -> Unit raise {
validate_invocations(suite, entries, definitions)
outcomes.validate()
if outcomes.calls.length() != suite.calls.length() {
raise Invalid("execution.result_count")
}
if outcomes.extensions != suite.extensions {
raise Invalid("execution.outcome_extensions")
}
for i = 0; i < outcomes.calls.length(); i = i + 1 {
let call = outcomes.calls[i]
if call.id != suite.calls[i].id {
raise Invalid("execution.call_identity")
}
let entry = entries.filter(e => e.name == suite.calls[i].entry)[0]
match call.outcome {
Returned(value) =>
validate_as(value, entry.output, definitions, output=true)
ModelError(code) =>
validate_as(
Value::ModelError(code),
entry.output,
definitions,
output=true,
)
Failed(_) | Cancelled => ()
}
}
}