///|
/// Canonical JSON interchange between this library and the Rust oracle.
///
/// The field names and shapes match `oracle/src/main.rs`; both sides must
/// stay in sync for differential tests to be meaningful.
///|
fn jstr(s : String) -> Json {
Json::string(s)
}
///|
fn jopt(s : String?) -> Json {
match s {
Some(v) => Json::string(v)
None => Json::null()
}
}
///|
fn jstrs(values : Array[String]) -> Json {
let out : Array[Json] = []
for v in values {
out.push(Json::string(v))
}
Json::array(out)
}
///|
fn jcategory(c : Category) -> Json {
let obj : Map[String, Json] = Map([])
obj["domain"] = jopt(c.domain)
obj["name"] = jstr(c.name)
Json::object(obj)
}
///|
fn jcategories(cs : Array[Category]) -> Json {
let out : Array[Json] = []
for c in cs {
out.push(jcategory(c))
}
Json::array(out)
}
///|
fn jextensions(exts : ExtensionMap) -> Json {
let outer : Map[String, Json] = Map([])
for ns, inner in exts {
let mid : Map[String, Json] = Map([])
for name, list in inner {
let arr : Array[Json] = []
for ext in list {
arr.push(jextension(ext))
}
mid[name] = Json::array(arr)
}
outer[ns] = Json::object(mid)
}
Json::object(outer)
}
///|
fn jextension(ext : Extension) -> Json {
let attrs : Map[String, Json] = Map([])
for k, v in ext.attrs {
attrs[k] = jstr(v)
}
let children : Map[String, Json] = Map([])
for name, list in ext.children {
let arr : Array[Json] = []
for child in list {
arr.push(jextension(child))
}
children[name] = Json::array(arr)
}
let obj : Map[String, Json] = Map([])
obj["name"] = jstr(ext.name)
obj["value"] = jopt(ext.value)
obj["attrs"] = Json::object(attrs)
obj["children"] = Json::object(children)
Json::object(obj)
}
///|
fn jatom_links(links : Array[@atom.Link]) -> Json {
let arr : Array[Json] = []
for link in links {
let obj : Map[String, Json] = Map([])
obj["href"] = jstr(link.href)
obj["rel"] = jopt(link.rel)
obj["type"] = jopt(link.media_type)
obj["hreflang"] = jopt(link.hreflang)
obj["title"] = jopt(link.title)
obj["length"] = jopt(link.length)
arr.push(Json::object(obj))
}
Json::array(arr)
}
///|
fn jitunes_channel(ext : ITunesChannelExtension?) -> Json {
match ext {
None => Json::null()
Some(ext) => {
let obj : Map[String, Json] = Map([])
obj["author"] = jopt(ext.author)
obj["block"] = jopt(ext.block)
let cats : Array[Json] = []
for category in ext.categories {
let cobj : Map[String, Json] = Map([])
cobj["text"] = jstr(category.text)
cobj["subcategory"] = match category.subcategory {
Some(sub) => {
let sobj : Map[String, Json] = Map([])
sobj["text"] = jstr(sub.text)
sobj["subcategory"] = Json::null()
Json::object(sobj)
}
None => Json::null()
}
cats.push(Json::object(cobj))
}
obj["categories"] = Json::array(cats)
obj["image"] = jopt(ext.image)
obj["explicit"] = jopt(ext.explicit)
obj["complete"] = jopt(ext.complete)
obj["newFeedUrl"] = jopt(ext.new_feed_url)
obj["owner"] = match ext.owner {
Some(owner) => {
let oobj : Map[String, Json] = Map([])
oobj["name"] = jopt(owner.name)
oobj["email"] = jopt(owner.email)
Json::object(oobj)
}
None => Json::null()
}
obj["subtitle"] = jopt(ext.subtitle)
obj["summary"] = jopt(ext.summary)
obj["keywords"] = jopt(ext.keywords)
obj["type"] = jopt(ext.itunes_type)
Json::object(obj)
}
}
}
///|
fn jitunes_item(ext : ITunesItemExtension?) -> Json {
match ext {
None => Json::null()
Some(ext) => {
let obj : Map[String, Json] = Map([])
obj["author"] = jopt(ext.author)
obj["block"] = jopt(ext.block)
obj["image"] = jopt(ext.image)
obj["duration"] = jopt(ext.duration)
obj["explicit"] = jopt(ext.explicit)
obj["closedCaptioned"] = jopt(ext.closed_captioned)
obj["order"] = jopt(ext.order)
obj["subtitle"] = jopt(ext.subtitle)
obj["summary"] = jopt(ext.summary)
obj["keywords"] = jopt(ext.keywords)
obj["episode"] = jopt(ext.episode)
obj["season"] = jopt(ext.season)
obj["episodeType"] = jopt(ext.episode_type)
Json::object(obj)
}
}
}
///|
fn jdublincore(ext : DublinCoreExtension?) -> Json {
match ext {
None => Json::null()
Some(ext) => {
let obj : Map[String, Json] = Map([])
obj["contributors"] = jstrs(ext.contributors)
obj["coverages"] = jstrs(ext.coverages)
obj["creators"] = jstrs(ext.creators)
obj["dates"] = jstrs(ext.dates)
obj["descriptions"] = jstrs(ext.descriptions)
obj["formats"] = jstrs(ext.formats)
obj["identifiers"] = jstrs(ext.identifiers)
obj["languages"] = jstrs(ext.languages)
obj["publishers"] = jstrs(ext.publishers)
obj["relations"] = jstrs(ext.relations)
obj["rights"] = jstrs(ext.rights)
obj["sources"] = jstrs(ext.sources)
obj["subjects"] = jstrs(ext.subjects)
obj["titles"] = jstrs(ext.titles)
obj["types"] = jstrs(ext.types)
Json::object(obj)
}
}
}
///|
fn jsyndication(ext : SyndicationExtension?) -> Json {
match ext {
None => Json::null()
Some(ext) => {
let obj : Map[String, Json] = Map([])
obj["period"] = jstr(ext.period.to_string())
obj["frequency"] = Json::number(ext.frequency.to_double())
obj["base"] = jstr(ext.base)
Json::object(obj)
}
}
}
///|
fn jitem(item : Item) -> Json {
let obj : Map[String, Json] = Map([])
obj["title"] = jopt(item.title)
obj["link"] = jopt(item.link)
obj["description"] = jopt(item.description)
obj["author"] = jopt(item.author)
obj["categories"] = jcategories(item.categories)
obj["comments"] = jopt(item.comments)
obj["enclosure"] = match item.enclosure {
Some(e) => {
let eobj : Map[String, Json] = Map([])
eobj["url"] = jstr(e.url)
eobj["length"] = jstr(e.length)
eobj["mimeType"] = jstr(e.mime_type)
Json::object(eobj)
}
None => Json::null()
}
obj["guid"] = match item.guid {
Some(g) => {
let gobj : Map[String, Json] = Map([])
gobj["isPermalink"] = Json::boolean(g.is_permalink)
gobj["value"] = jstr(g.value)
Json::object(gobj)
}
None => Json::null()
}
obj["pubDate"] = jopt(item.pub_date)
obj["source"] = match item.source {
Some(s) => {
let sobj : Map[String, Json] = Map([])
sobj["url"] = jstr(s.url)
sobj["title"] = jopt(s.title)
Json::object(sobj)
}
None => Json::null()
}
obj["content"] = jopt(item.content)
obj["extensions"] = jextensions(item.extensions)
obj["atomLinks"] = jatom_links(item.atom_links)
obj["itunes"] = jitunes_item(item.itunes_ext)
obj["dublincore"] = jdublincore(item.dublin_core_ext)
Json::object(obj)
}
///|
/// Convert a parsed channel to the canonical JSON form.
pub fn Channel::to_canon(self : Channel) -> Json {
let obj : Map[String, Json] = Map([])
obj["title"] = jstr(self.title)
obj["link"] = jstr(self.link)
obj["description"] = jstr(self.description)
obj["language"] = jopt(self.language)
obj["copyright"] = jopt(self.copyright)
obj["managingEditor"] = jopt(self.managing_editor)
obj["webMaster"] = jopt(self.webmaster)
obj["pubDate"] = jopt(self.pub_date)
obj["lastBuildDate"] = jopt(self.last_build_date)
obj["categories"] = jcategories(self.categories)
obj["generator"] = jopt(self.generator)
obj["docs"] = jopt(self.docs)
obj["cloud"] = match self.cloud {
Some(c) => {
let cobj : Map[String, Json] = Map([])
cobj["domain"] = jstr(c.domain)
cobj["port"] = jstr(c.port)
cobj["path"] = jstr(c.path)
cobj["registerProcedure"] = jstr(c.register_procedure)
cobj["protocol"] = jstr(c.protocol)
Json::object(cobj)
}
None => Json::null()
}
obj["rating"] = jopt(self.rating)
obj["ttl"] = jopt(self.ttl)
obj["image"] = match self.image {
Some(i) => {
let iobj : Map[String, Json] = Map([])
iobj["url"] = jstr(i.url)
iobj["title"] = jstr(i.title)
iobj["link"] = jstr(i.link)
iobj["width"] = jopt(i.width)
iobj["height"] = jopt(i.height)
iobj["description"] = jopt(i.description)
Json::object(iobj)
}
None => Json::null()
}
obj["textInput"] = match self.text_input {
Some(t) => {
let tobj : Map[String, Json] = Map([])
tobj["title"] = jstr(t.title)
tobj["description"] = jstr(t.description)
tobj["name"] = jstr(t.name)
tobj["link"] = jstr(t.link)
Json::object(tobj)
}
None => Json::null()
}
obj["skipHours"] = jstrs(self.skip_hours)
obj["skipDays"] = jstrs(self.skip_days)
let items : Array[Json] = []
for item in self.items {
items.push(jitem(item))
}
obj["items"] = Json::array(items)
obj["extensions"] = jextensions(self.extensions)
obj["atomLinks"] = jatom_links(self.atom_links)
obj["itunes"] = jitunes_channel(self.itunes_ext)
obj["dublincore"] = jdublincore(self.dublin_core_ext)
obj["syndication"] = jsyndication(self.syndication_ext)
let nss : Map[String, Json] = Map([])
for k, v in self.namespaces {
nss[k] = jstr(v)
}
obj["namespaces"] = Json::object(nss)
Json::object(obj)
}
///|
/// Recursively sort JSON object keys so that output is byte-identical to
/// serde_json's BTreeMap-based serialisation used by the oracle.
pub fn sort_json_keys(j : Json) -> Json {
match j {
Object(map) => {
let out : Map[String, Json] = Map([])
for k in sorted_keys(map) {
out[k] = sort_json_keys(map[k])
}
Json::object(out)
}
Array(arr) => {
let out : Array[Json] = []
for item in arr {
out.push(sort_json_keys(item))
}
Json::array(out)
}
other => other
}
}
///|
/// Compact, deterministic serialisation: object keys are emitted in sorted
/// order (matching serde_json over BTreeMaps).
pub fn canonical_stringify(j : Json) -> String {
let sb = StringBuilder()
write_canonical(sb, j)
sb.to_string()
}
///|
fn write_canonical(sb : StringBuilder, j : Json) -> Unit {
match j {
Null => sb.write_string("null")
True => sb.write_string("true")
False => sb.write_string("false")
String(s) => write_json_string(sb, s)
Number(_, repr~) =>
match repr {
Some(r) => sb.write_string(r)
None => sb.write_string(j.stringify())
}
Array(arr) => {
sb.write_string("[")
for i, item in arr {
if i > 0 {
sb.write_string(",")
}
write_canonical(sb, item)
}
sb.write_string("]")
}
Object(map) => {
sb.write_string("{")
for i, k in sorted_keys(map) {
if i > 0 {
sb.write_string(",")
}
write_json_string(sb, k)
sb.write_string(":")
write_canonical(sb, map[k])
}
sb.write_string("}")
}
}
}
///|
fn write_json_string(sb : StringBuilder, s : String) -> Unit {
sb.write_string("\"")
for c in s {
match c {
'"' => sb.write_string("\\\"")
'\\' => sb.write_string("\\\\")
'\n' => sb.write_string("\\n")
'\r' => sb.write_string("\\r")
'\t' => sb.write_string("\\t")
c =>
if c.to_int() < 0x20 {
let hex = "0123456789abcdef"
let v = c.to_int()
sb.write_string(
"\\u\{hex[(v >> 12) & 15]}\{hex[(v >> 8) & 15]}\{hex[(v >> 4) & 15]}\{hex[v & 15]}",
)
} else {
sb.write_char(c)
}
}
}
sb.write_string("\"")
}