///|
/// The `STATUS` aggregate OFX attaches to a response.
///
/// `code` is the numeric result: zero means success, and a negative or positive
/// value is the server's own error code. `severity` is the document's own
/// `INFO`/`WARN`/`ERROR` label, kept as written so a caller can compare it with
/// the code instead of trusting either one alone.
pub struct Status {
code : Int
severity : String
message : String
}
///|
/// Whether the status code means success.
pub fn Status::is_success(self : Status) -> Bool {
self.code == 0
}
///|
/// Whether the document labelled the status as an error, without regard to
/// case.
pub fn Status::is_error(self : Status) -> Bool {
self.severity.to_upper() == "ERROR"
}
///|
/// The status as `severity:code: message`, the form a log line wants.
pub fn Status::to_string(self : Status) -> String {
let label = if self.severity == "" { "INFO" } else { self.severity }
let detail = if self.message == "" { "" } else { ": " + self.message }
label + ":" + self.code.to_string() + detail
}
///|
/// The `SONRS` sign-on response: when the file was produced, which financial
/// institution produced it, and whether that server considered the response a
/// success.
///
/// A statement whose sign-on reports an error is still parsed — the
/// transactions are usually there — but the caller should know that the server
/// itself said something went wrong, so the error is also raised as a
/// diagnostic.
pub struct Signon {
server_date : String
language : String
organisation : String
institution_id : String
session_cookie : String
status : Status?
}
///|
/// Whether the document carried a `SONRS` aggregate at all.
pub fn Signon::is_present(self : Signon) -> Bool {
self.server_date != "" ||
self.language != "" ||
self.organisation != "" ||
self.institution_id != "" ||
self.session_cookie != "" ||
self.status is Some(_)
}
///|
/// The `DTSERVER` instant, or `None` when it is absent or not a real date.
pub fn Signon::server_time(self : Signon) -> PostedDate? {
parse_posted_date(self.server_date)
}
///|
/// The sign-on as one line for a report, or the empty string when the document
/// declared none.
pub fn Signon::to_string(self : Signon) -> String {
if !self.is_present() {
return ""
}
let parts : Array[String] = []
if self.server_date != "" {
parts.push("server=" + self.server_date)
}
if self.organisation != "" {
parts.push("fi=" + self.organisation)
}
if self.institution_id != "" {
parts.push("fid=" + self.institution_id)
}
if self.language != "" {
parts.push("language=" + self.language)
}
match self.status {
Some(status) => parts.push("status=" + status.to_string())
None => ()
}
parts.join(" ")
}
///|
/// Read the sign-on response out of a token stream.
///
/// This runs as its own pass over the tokens rather than inside the statement
/// loop, because the aggregates it cares about (`SONRS`, `FI`, `STATUS`) nest
/// and have nothing to do with accounts or transactions. Both dialects are
/// handled: an XML document closes its aggregates, and an SGML document is
/// scoped by the next aggregate that starts.
fn parse_signon(
tokens : Array[TagToken],
diagnostics : Array[Diagnostic],
) -> Signon {
let mut server_date = ""
let mut server_date_line = 0
let mut language = ""
let mut session_cookie = ""
let mut organisation = ""
let mut institution_id = ""
let mut code_text = ""
let mut code_line = 0
let mut severity = ""
let mut message = ""
let mut in_fi = false
let mut in_status = false
for token in tokens {
let tag = token.name
match close_target(tag) {
Some(target) =>
if target == "FI" {
in_fi = false
} else if target == "STATUS" {
in_status = false
} else if target == "SONRS" {
in_fi = false
in_status = false
}
None =>
match tag {
"SONRS" => {
in_fi = false
in_status = false
}
"FI" => {
in_fi = true
in_status = false
}
"STATUS" => {
in_status = true
in_fi = false
}
"DTSERVER" => {
server_date = token.value
server_date_line = token.line
}
"LANGUAGE" => language = token.value
"SESSCOOKIE" => session_cookie = token.value
"ORG" => if in_fi { organisation = token.value }
"FID" => if in_fi { institution_id = token.value }
"CODE" =>
if in_status {
code_text = token.value
code_line = token.line
}
"SEVERITY" => if in_status { severity = token.value }
"MESSAGE" => if in_status { message = token.value }
_ => ()
}
}
}
let mut status : Status? = None
if code_text != "" || severity != "" || message != "" {
match parse_int(code_text) {
Some(code) => status = Some({ code, severity, message, })
None =>
diagnostics.push(
warning_at("invalid-status-code", at_text(code_text, code_line)),
)
}
}
if server_date != "" && !valid_date(server_date) {
diagnostics.push(
warning_at("invalid-server-date", at_line("DTSERVER", server_date_line)),
)
}
match status {
Some(value) =>
if value.is_error() {
diagnostics.push(warning_at("signon-status-error", value.to_string()))
}
None => ()
}
{
server_date,
language,
organisation,
institution_id,
session_cookie,
status,
}
}