///|
fn table_error(table : String, code : String, message : String) -> Diagnostic {
issue("error", code, table, message)
}
///|
fn expect_headers(
table : CsvTable,
name : String,
headers : Array[String],
) -> Diagnostic? {
if table.has_exact_headers(headers) {
None
} else {
Some(
table_error(
name,
"csv-schema",
"expected columns in order: \{headers.join(",")}",
),
)
}
}
///|
fn csv_int(
cell : String,
file : String,
line : Int32,
field : String,
findings : Array[Diagnostic],
) -> Int64? {
let value = @string.parse_int64(cell.trim()) catch {
_ => {
findings.push(
issue(
"error",
"csv-invalid-integer",
"\{file}:\{line}",
"\{field} must be an exact signed integer",
),
)
return None
}
}
Some(value)
}
///|
fn parse_lots_csv(table : CsvTable, findings : Array[Diagnostic]) -> Array[Lot] {
let lots : Array[Lot] = []
for row in table.rows {
match
(
csv_int(row.cells[3], "lots.csv", row.line, "quantity_g", findings),
csv_int(row.cells[4], "lots.csv", row.line, "produced_at", findings),
) {
(Some(quantity_g), Some(produced_at)) =>
lots.push({
id: row.cells[0],
product: row.cells[1],
facility: row.cells[2],
quantity_g,
produced_at,
})
_ => ()
}
}
lots
}
///|
fn parse_transformations_csv(
table : CsvTable,
findings : Array[Diagnostic],
) -> Array[Transformation] {
let transformations : Array[Transformation] = []
let positions : Map[String, Int32] = Map([])
for row in table.rows {
let id = row.cells[0]
let facility = row.cells[1]
let time = csv_int(
row.cells[2],
"transformations.csv",
row.line,
"occurred_at",
findings,
)
let direction = row.cells[3]
let lot_id = row.cells[4]
let quantity = csv_int(
row.cells[5],
"transformations.csv",
row.line,
"quantity_g",
findings,
)
if direction != "input" && direction != "output" {
findings.push(
issue(
"error",
"csv-invalid-direction",
"transformations.csv:\{row.line}",
"direction must be input or output",
),
)
continue
}
match (time, quantity) {
(Some(occurred_at), Some(quantity_g)) => {
let position = match positions.get(id) {
Some(existing) => existing
None => {
let next = transformations.length()
transformations.push({
id,
facility,
occurred_at,
inputs: [],
outputs: [],
})
positions[id] = next
next
}
}
let existing = transformations[position]
if existing.facility != facility || existing.occurred_at != occurred_at {
findings.push(
issue(
"error",
"csv-event-conflict",
"transformations.csv:\{row.line}",
"same event id has conflicting facility or time",
),
)
continue
}
let portion : Portion = { lot_id, quantity_g, }
if direction == "input" {
existing.inputs.push(portion)
} else {
existing.outputs.push(portion)
}
}
_ => ()
}
}
transformations
}
///|
fn parse_shipments_csv(
table : CsvTable,
findings : Array[Diagnostic],
) -> Array[Shipment] {
let shipments : Array[Shipment] = []
for row in table.rows {
match
(
csv_int(row.cells[2], "shipments.csv", row.line, "quantity_g", findings),
csv_int(row.cells[4], "shipments.csv", row.line, "shipped_at", findings),
) {
(Some(quantity_g), Some(shipped_at)) =>
shipments.push({
id: row.cells[0],
lot_id: row.cells[1],
quantity_g,
consignee: row.cells[3],
shipped_at,
reference: row.cells[5],
})
_ => ()
}
}
shipments
}
///|
/// Import three small operational CSV tables into the same typed ledger used
/// by the JSON API. Every numeric value remains an exact integer.
pub fn import_csv(
lots_csv : String,
transformations_csv : String,
shipments_csv : String,
) -> Result[Ledger, ValidationReport] {
let findings : Array[Diagnostic] = []
let lots_table = match parse_csv(lots_csv) {
Ok(table) => Some(table)
Err(error) => {
findings.push(error)
None
}
}
let transformations_table = match parse_csv(transformations_csv) {
Ok(table) => Some(table)
Err(error) => {
findings.push(error)
None
}
}
let shipments_table = match parse_csv(shipments_csv) {
Ok(table) => Some(table)
Err(error) => {
findings.push(error)
None
}
}
if lots_table is Some(table) {
if expect_headers(table, "lots.csv", [
"id", "product", "facility", "quantity_g", "produced_at",
])
is Some(error) {
findings.push(error)
}
}
if transformations_table is Some(table) {
if expect_headers(table, "transformations.csv", [
"id", "facility", "occurred_at", "direction", "lot_id", "quantity_g",
])
is Some(error) {
findings.push(error)
}
}
if shipments_table is Some(table) {
if expect_headers(table, "shipments.csv", [
"id", "lot_id", "quantity_g", "consignee", "shipped_at", "reference",
])
is Some(error) {
findings.push(error)
}
}
if !findings.is_empty() {
return Err(report_from(findings))
}
let lots = match lots_table {
Some(table) => parse_lots_csv(table, findings)
None => []
}
let transformations = match transformations_table {
Some(table) => parse_transformations_csv(table, findings)
None => []
}
let shipments = match shipments_table {
Some(table) => parse_shipments_csv(table, findings)
None => []
}
if !findings.is_empty() {
return Err(report_from(findings))
}
let ledger : Ledger = { schema_version: 1, lots, transformations, shipments, }
let validation = validate_ledger(ledger)
if validation.valid {
Ok(ledger)
} else {
Err(validation)
}
}