///|
pub(all) struct JobSpec {
sections : Array[Section]
maximum : Int32
policy : SignaturePolicy
capacities : Array[Int32]?
binding : Binding
duplex : Duplex
paper : Paper
print : PrintOptions
}
///|
pub(all) struct PrintJob {
booklet : Booklet
paper : Paper
schedule : PrintSchedule
}
///|
priv suberror JobDecode {
JobDecode(WorkflowError)
}
///|
fn decode_fail(path : String, message : String) -> Unit raise JobDecode {
raise JobDecode(workflow_error(path, message))
}
///|
fn read_object(
value : Json,
path : String,
allowed : Array[String],
) -> Map[String, Json] raise JobDecode {
let fields = match value {
Object(fields) => fields
_ => {
decode_fail(path, "expected an object")
Map([])
}
}
for key, _ in fields {
if !allowed.contains(key) {
decode_fail(path + "." + key, "unknown field")
}
}
fields
}
///|
fn required(
fields : Map[String, Json],
key : String,
path : String,
) -> Json raise JobDecode {
match fields.get(key) {
Some(value) => value
None => {
decode_fail(path + "." + key, "required field missing")
Json::null()
}
}
}
///|
fn read_integer(
value : Json,
low : Int32,
high : Int32,
path : String,
) -> Int32 raise JobDecode {
match value {
Number(number, ..) => {
if number < low.to_double() || number > high.to_double() {
decode_fail(path, "integer outside \{low}..\{high}")
}
let integer = number.to_int()
if integer.to_double() != number {
decode_fail(path, "expected a whole number")
}
integer
}
_ => {
decode_fail(path, "expected an integer")
0
}
}
}
///|
fn integer_field(
fields : Map[String, Json],
key : String,
fallback : Int32?,
low : Int32,
high : Int32,
path : String,
) -> Int32 raise JobDecode {
match (fields.get(key), fallback) {
(Some(value), _) => read_integer(value, low, high, path + "." + key)
(None, Some(value)) => value
(None, None) => {
decode_fail(path + "." + key, "required integer missing")
0
}
}
}
///|
fn text_field(
fields : Map[String, Json],
key : String,
fallback : String?,
path : String,
) -> String raise JobDecode {
match (fields.get(key), fallback) {
(Some(String(value)), _) => value
(None, Some(value)) => value
_ => {
decode_fail(path + "." + key, "expected a string")
""
}
}
}
///|
fn bool_field(
fields : Map[String, Json],
key : String,
fallback : Bool,
path : String,
) -> Bool raise JobDecode {
match fields.get(key) {
Some(True) => true
Some(False) => false
None => fallback
_ => {
decode_fail(path + "." + key, "expected a boolean")
false
}
}
}
///|
fn decode_job(value : Json) -> JobSpec raise JobDecode {
let root = read_object(value, "$", [
"schema", "sections", "maximum", "policy", "gatherings", "binding", "duplex",
"paper", "print",
])
let _schema = integer_field(root, "schema", Some(1), 1, 1, "$")
let sections : Array[Section] = []
let items = match required(root, "sections", "$") {
Array(items) => items
_ => {
decode_fail("$.sections", "expected an array")
[]
}
}
if items.is_empty() || items.length() > 1000 {
decode_fail("$.sections", "expected 1..1000 sections")
}
for index, item in items {
let path = "$.sections[\{index}]"
let fields = read_object(item, path, [
"source", "source_pages", "selection", "start_on_recto",
])
sections.push({
source: text_field(fields, "source", None, path),
source_pages: integer_field(fields, "source_pages", None, 1, 100000, path),
selection: text_field(fields, "selection", None, path),
start_on_recto: bool_field(fields, "start_on_recto", false, path),
})
}
let maximum = integer_field(root, "maximum", Some(16), 4, 128, "$")
let policy = match text_field(root, "policy", Some("balanced"), "$") {
"fill" => Fill
"balanced" => Balanced
_ => {
decode_fail("$.policy", "expected fill or balanced")
Fill
}
}
let capacities = match root.get("gatherings") {
None => None
Some(Array(items)) => {
if root.contains("maximum") || root.contains("policy") {
decode_fail(
"$.gatherings", "cannot combine explicit gatherings with maximum or policy",
)
}
if items.is_empty() || items.length() > 25000 {
decode_fail("$.gatherings", "expected 1..25000 capacities")
}
let sizes : Array[Int32] = []
for index, item in items {
sizes.push(read_integer(item, 4, 128, "$.gatherings[\{index}]"))
}
Some(sizes)
}
_ => {
decode_fail("$.gatherings", "expected an array")
None
}
}
let binding = match text_field(root, "binding", Some("left"), "$") {
"left" => Left
"right" => Right
_ => {
decode_fail("$.binding", "expected left or right")
Left
}
}
let duplex = match text_field(root, "duplex", Some("long-edge"), "$") {
"long-edge" => LongEdge
"short-edge" => ShortEdge
_ => {
decode_fail("$.duplex", "expected long-edge or short-edge")
LongEdge
}
}
let p = read_object(required(root, "paper", "$"), "$.paper", [
"width", "height", "margin", "gutter", "creep",
])
let paper : Paper = {
width: integer_field(p, "width", None, 1, 10000, "$.paper"),
height: integer_field(p, "height", None, 1, 10000, "$.paper"),
margin: integer_field(p, "margin", Some(10), 0, 10000, "$.paper"),
gutter: integer_field(p, "gutter", Some(0), 0, 10000, "$.paper"),
creep: integer_field(p, "creep", Some(0), 0, 1000, "$.paper"),
}
let print = read_object(
root.get("print").unwrap_or(Json::empty_object()),
"$.print",
["copies", "reverse_sheets", "collation", "mode", "back_stack"],
)
let collation = match
text_field(print, "collation", Some("collated"), "$.print") {
"collated" => Collated
"uncollated" => Uncollated
_ => {
decode_fail("$.print.collation", "expected collated or uncollated")
Collated
}
}
let mode = match text_field(print, "mode", Some("automatic"), "$.print") {
"automatic" => {
if print.contains("back_stack") {
decode_fail("$.print.back_stack", "only valid for manual printing")
}
Automatic
}
"manual" =>
Manual(
match text_field(print, "back_stack", Some("reversed"), "$.print") {
"same" => SameOrder
"reversed" => Reversed
_ => {
decode_fail("$.print.back_stack", "expected same or reversed")
Reversed
}
},
)
_ => {
decode_fail("$.print.mode", "expected automatic or manual")
Automatic
}
}
{
sections,
maximum,
policy,
capacities,
binding,
duplex,
paper,
print: {
copies: integer_field(print, "copies", Some(1), 1, 1000, "$.print"),
reverse_sheets: bool_field(print, "reverse_sheets", false, "$.print"),
collation,
mode,
},
}
}
///|
#warnings("-deprecated")
pub fn parse_job(text : String) -> Result[JobSpec, WorkflowError] {
if text.length() > 2000000 {
return Err(workflow_error("$", "job input exceeds 2000000 code units"))
}
let value = @json.parse(text, max_nesting_depth=32) catch {
_ => return Err(workflow_error("$", "invalid JSON or nesting exceeds 32"))
}
Ok(decode_job(value)) catch {
JobDecode(error) => Err(error)
}
}
///|
/// Compile a request only when the document, imposition, geometry and schedule pass.
pub fn build_job(spec : JobSpec) -> Result[PrintJob, WorkflowError] {
let document = match assemble_document(spec.sections) {
Ok(doc) => doc
Err(e) => return Err(e)
}
let groups = match
(match spec.capacities {
Some(sizes) => explicit_gatherings(document.pages.length(), sizes)
None =>
plan_gatherings(document.pages.length(), spec.maximum, spec.policy)
}) {
Ok(groups) => groups
Err(e) => return Err(e)
}
let booklet = match
impose_document(document, groups, spec.binding, spec.duplex) {
Ok(booklet) => booklet
Err(e) => return Err(e)
}
let check = match preflight(booklet, spec.paper) {
Ok(report) => report
Err(e) => return Err(e)
}
for issue in check.issues {
if issue.blocking {
return Err(workflow_error(issue.field, issue.message))
}
}
let schedule = match schedule_print(booklet, spec.print) {
Ok(schedule) => schedule
Err(e) => return Err(e)
}
match verify_schedule(booklet, schedule) {
Ok(_) => ()
Err(e) => return Err(e)
}
Ok({ booklet, paper: spec.paper, schedule, })
}