///|
fn json_int(value : Int) -> Json {
Json::number(value.to_double())
}
///|
fn json_field(json : Json, field : String) -> Json raise IppError {
match json {
Object(fields) =>
match fields.get(field) {
Some(value) => value
None => raise Invalid("missing JSON field: \{field}")
}
_ => raise Invalid("JSON object expected")
}
}
///|
fn json_as_int(json : Json) -> Int raise IppError {
match json {
Number(number, ..) => {
if number < -2147483648.0 ||
number > 2147483647.0 ||
number != number.floor() {
raise Invalid("JSON integer out of range")
}
number.to_int()
}
_ => raise Invalid("JSON integer expected")
}
}
///|
fn json_as_string(json : Json) -> String raise IppError {
match json {
String(text) => text
_ => raise Invalid("JSON string expected")
}
}
///|
fn json_as_array(json : Json) -> Array[Json] raise IppError {
match json {
Array(values) => values
_ => raise Invalid("JSON array expected")
}
}
///|
fn json_get_int(json : Json, field : String) -> Int raise IppError {
json_as_int(json_field(json, field))
}
///|
fn json_get_string(json : Json, field : String) -> String raise IppError {
json_as_string(json_field(json, field))
}
///|
pub fn IppValue::to_json(self : IppValue) -> Json {
let value : Json = match self {
Integer(n) | Enumeration(n) => json_int(n)
Boolean(b) => Json::boolean(b)
Range(lower, upper) =>
{ "lower": json_int(lower), "upper": json_int(upper) }
ResolutionValue(resolution) =>
{
"x": json_int(resolution.x),
"y": json_int(resolution.y),
"units": json_int(resolution.units),
}
DateTime(date) =>
{
"year": json_int(date.year),
"month": json_int(date.month),
"day": json_int(date.day),
"hour": json_int(date.hour),
"minute": json_int(date.minute),
"second": json_int(date.second),
"decisecond": json_int(date.decisecond),
"utc_direction": json_int(date.utc_direction),
"utc_hours": json_int(date.utc_hours),
"utc_minutes": json_int(date.utc_minutes),
}
Text(text)
| Name(text)
| Keyword(text)
| Uri(text)
| UriScheme(text)
| Charset(text)
| Language(text)
| MimeType(text) => Json::string(text)
Octets(bytes) | Unknown(_, bytes) => Json::string(bytes_to_hex(bytes))
TextWithLanguage(language, text) | NameWithLanguage(language, text) =>
{ "language": Json::string(language), "text": Json::string(text) }
Collection(members) =>
Json::array(members.map(fn(field) { field.to_json() }))
OutOfBand(_) => Json::null()
}
{
"tag": json_int(self.tag()),
"type": Json::string(value_tag_name(self.tag())),
"value": value,
}
}
///|
pub fn Attribute::to_json(self : Attribute) -> Json {
{
"name": Json::string(self.name),
"values": Json::array(self.values.map(fn(value) { value.to_json() })),
}
}
///|
pub fn Group::to_json(self : Group) -> Json {
{
"tag": json_int(self.tag),
"name": Json::string(group_name(self.tag)),
"attributes": Json::array(
self.attributes.map(fn(attribute) { attribute.to_json() }),
),
}
}
///|
pub fn Message::to_json(self : Message) -> Json {
{
"version": {
"major": json_int(self.version.major),
"minor": json_int(self.version.minor),
},
"code": json_int(self.code),
"request_id": json_int(self.request_id),
"groups": Json::array(self.groups.map(fn(group) { group.to_json() })),
"document_hex": Json::string(bytes_to_hex(self.document)),
}
}
///|
fn value_from_json(
json : Json,
budget : DecodeBudget,
depth : Int,
) -> IppValue raise IppError {
budget.value()
let tag = json_get_int(json, "tag")
let value = json_field(json, "value")
match tag {
0x21 => Integer(json_as_int(value))
0x22 =>
match value {
True => Boolean(true)
False => Boolean(false)
_ => raise Invalid("boolean JSON value expected")
}
0x23 => {
let code = json_as_int(value)
if code < 1 {
raise Invalid("enum must be positive")
}
Enumeration(code)
}
0x30 => Octets(bytes_from_hex(json_as_string(value)))
0x31 => {
let date : IppDateTime = {
year: json_get_int(value, "year"),
month: json_get_int(value, "month"),
day: json_get_int(value, "day"),
hour: json_get_int(value, "hour"),
minute: json_get_int(value, "minute"),
second: json_get_int(value, "second"),
decisecond: json_get_int(value, "decisecond"),
utc_direction: json_get_int(value, "utc_direction"),
utc_hours: json_get_int(value, "utc_hours"),
utc_minutes: json_get_int(value, "utc_minutes"),
}
date.validate()
DateTime(date)
}
0x32 => {
let resolution : Resolution = {
x: json_get_int(value, "x"),
y: json_get_int(value, "y"),
units: json_get_int(value, "units"),
}
let result = IppValue::ResolutionValue(resolution)
let _ = encode_scalar(result)
result
}
0x33 => {
let lower = json_get_int(value, "lower")
let upper = json_get_int(value, "upper")
if lower > upper {
raise Invalid("reversed range")
}
Range(lower, upper)
}
0x34 => {
if depth >= budget.limits.max_collection_depth {
raise Limit("JSON collection nesting")
}
let members : Array[Attribute] = []
for field in json_as_array(value) {
members.push(attribute_from_json(field, budget, depth + 1))
}
Collection(members)
}
0x35 =>
TextWithLanguage(
json_get_string(value, "language"),
json_get_string(value, "text"),
)
0x36 =>
NameWithLanguage(
json_get_string(value, "language"),
json_get_string(value, "text"),
)
0x41 => Text(json_as_string(value))
0x42 => Name(json_as_string(value))
0x44 => Keyword(json_as_string(value))
0x45 => Uri(json_as_string(value))
0x46 => UriScheme(json_as_string(value))
0x47 => Charset(json_as_string(value))
0x48 => Language(json_as_string(value))
0x49 => MimeType(json_as_string(value))
0x10 | 0x12 | 0x13 | 0x15 | 0x16 | 0x17 => {
if value != Json::null() {
raise Invalid("out-of-band JSON value must be null")
}
OutOfBand(tag)
}
_ => {
let result = IppValue::Unknown(tag, bytes_from_hex(json_as_string(value)))
let _ = encode_scalar(result)
if tag < 0x10 || tag > 0x7f || tag == 0x37 || tag == 0x4a {
raise Invalid("unsupported JSON value tag")
}
result
}
}
}
///|
fn attribute_from_json(
json : Json,
budget : DecodeBudget,
depth : Int,
) -> Attribute raise IppError {
budget.attribute()
let name = json_get_string(json, "name")
if !valid_attribute_name(name) {
raise Invalid("invalid JSON attribute name")
}
let values : Array[IppValue] = []
for item in json_as_array(json_field(json, "values")) {
values.push(value_from_json(item, budget, depth))
}
if values.is_empty() ||
values.any(fn(value) { value.tag() != values[0].tag() }) {
raise Invalid("JSON attribute requires nonempty homogeneous values")
}
{ name, values, }
}
///|
pub fn message_from_json(
json : Json,
limits? : DecodeLimits = DecodeLimits::defaults(),
) -> Message raise IppError {
limits.validate()
let budget : DecodeBudget = { limits, attributes: 0, values: 0, }
let version = json_field(json, "version")
let version = Version::new(
json_get_int(version, "major"),
json_get_int(version, "minor"),
)
let groups : Array[Group] = []
for json_group in json_as_array(json_field(json, "groups")) {
let tag = json_get_int(json_group, "tag")
if !valid_group_tag(tag) {
raise Invalid("invalid JSON group tag")
}
let attributes : Array[Attribute] = []
for item in json_as_array(json_field(json_group, "attributes")) {
attributes.push(attribute_from_json(item, budget, 0))
}
groups.push({ tag, attributes, })
}
let document_hex = json_get_string(json, "document_hex")
if document_hex.length() / 2 > limits.max_document_bytes {
raise Limit("JSON document bytes")
}
let message : Message = {
version,
code: json_get_int(json, "code"),
request_id: json_get_int(json, "request_id"),
groups,
document: bytes_from_hex(document_hex),
}
let _ = encode(message, limits~)
message
}
///|
pub fn Job::to_json(self : Job) -> Json {
let state = match self.state {
Some(state) => Json::string(state.name())
None => Json::null()
}
let uri = match self.uri {
Some(uri) => Json::string(uri)
None => Json::null()
}
let name = match self.name {
Some(name) => Json::string(name)
None => Json::null()
}
let message = match self.message {
Some(message) => Json::string(message)
None => Json::null()
}
let impressions = match self.impressions_completed {
Some(count) => json_int(count)
None => Json::null()
}
{
"id": json_int(self.id),
"uri": uri,
"name": name,
"state": state,
"reasons": Json::array(
self.reasons.map(fn(reason) { Json::string(reason) }),
),
"message": message,
"impressions_completed": impressions,
}
}
///|
pub fn Diagnostic::to_json(self : Diagnostic) -> Json {
{
"severity": Json::string(
if self.severity == Error {
"error"
} else {
"warning"
},
),
"attribute": Json::string(self.attribute),
"message": Json::string(self.message),
}
}