///|
fn malformed(stage : String, detail : String) -> AcmeError {
AcmeError::Malformed(stage~, detail~)
}
///|
fn object_at(value : Json, stage : String) -> Map[String, Json] raise AcmeError {
match value {
Object(fields) => fields
_ => raise malformed(stage, "expected a JSON object")
}
}
///|
fn required_string(
fields : Map[String, Json],
name : String,
stage : String,
) -> String raise AcmeError {
match fields.get(name) {
Some(String(value)) => value
Some(_) => raise malformed(stage, "field `\{name}` must be a string")
None => raise malformed(stage, "missing string field `\{name}`")
}
}
///|
fn optional_string(
fields : Map[String, Json],
name : String,
stage : String,
) -> String? raise AcmeError {
match fields.get(name) {
Some(String(value)) => Some(value)
Some(Null) | None => None
Some(_) => raise malformed(stage, "field `\{name}` must be a string")
}
}
///|
fn optional_bool(
fields : Map[String, Json],
name : String,
stage : String,
fallback : Bool,
) -> Bool raise AcmeError {
match fields.get(name) {
Some(True) => true
Some(False) => false
Some(Null) | None => fallback
Some(_) => raise malformed(stage, "field `\{name}` must be a boolean")
}
}
///|
fn string_array(
fields : Map[String, Json],
name : String,
stage : String,
) -> Array[String] raise AcmeError {
match fields.get(name) {
Some(Array(values)) => {
let out : Array[String] = []
for value in values {
match value {
String(text) => out.push(text)
_ => raise malformed(stage, "field `\{name}` must contain strings")
}
}
out
}
Some(_) => raise malformed(stage, "field `\{name}` must be an array")
None => raise malformed(stage, "missing array field `\{name}`")
}
}
///|
fn parse_json(input : String, stage : String) -> Json raise AcmeError {
@json.loads(input[:]) catch {
_ => raise malformed(stage, "invalid JSON")
}
}
///|
/// Decode an ACME directory document and its commonly used metadata.
pub fn Directory::decode(input : String) -> Directory raise AcmeError {
let fields = object_at(parse_json(input, "directory"), "directory")
let meta = match fields.get("meta") {
Some(value) => object_at(value, "directory.meta")
None => Map([])
}
Directory::{
new_nonce: required_string(fields, "newNonce", "directory"),
new_account: required_string(fields, "newAccount", "directory"),
new_order: required_string(fields, "newOrder", "directory"),
revoke_cert: required_string(fields, "revokeCert", "directory"),
key_change: required_string(fields, "keyChange", "directory"),
renewal_info: optional_string(fields, "renewalInfo", "directory"),
terms_of_service: optional_string(meta, "termsOfService", "directory.meta"),
website: optional_string(meta, "website", "directory.meta"),
external_account_required: optional_bool(
meta, "externalAccountRequired", "directory.meta", false,
),
}
}
///|
fn decode_identifier(
value : Json,
stage : String,
) -> Identifier raise AcmeError {
let fields = object_at(value, stage)
Identifier::{
kind: required_string(fields, "type", stage),
value: required_string(fields, "value", stage),
}
}
///|
fn identifier_array(
fields : Map[String, Json],
name : String,
stage : String,
) -> Array[Identifier] raise AcmeError {
match fields.get(name) {
Some(Array(values)) => {
let out : Array[Identifier] = []
for value in values {
out.push(decode_identifier(value, "\{stage}.\{name}"))
}
out
}
Some(_) => raise malformed(stage, "field `\{name}` must be an array")
None => raise malformed(stage, "missing array field `\{name}`")
}
}
///|
/// Decode an order body. The resource URL comes from the response Location
/// header and is supplied separately.
pub fn Order::decode(input : String, url~ : String) -> Order raise AcmeError {
let fields = object_at(parse_json(input, "order"), "order")
Order::{
url,
status: ResourceStatus::parse(required_string(fields, "status", "order")),
identifiers: identifier_array(fields, "identifiers", "order"),
authorization_urls: string_array(fields, "authorizations", "order"),
finalize_url: required_string(fields, "finalize", "order"),
certificate_url: optional_string(fields, "certificate", "order"),
expires: optional_string(fields, "expires", "order"),
problem: None,
}
}
///|
/// Encode the RFC 8555 new-order payload in deterministic key order.
pub fn encode_new_order(identifiers : Array[Identifier]) -> String {
let values : Array[Json] = []
for identifier in identifiers {
values.push(
Json::object({
"type": Json::string(identifier.kind),
"value": Json::string(identifier.value),
}),
)
}
@json.dumps(Json::object({ "identifiers": Json::array(values) }), sort=true)
}
///|
/// Encode a new-account payload. An empty contacts array is valid.
pub fn encode_new_account(
contacts : Array[String],
terms_agreed : Bool,
) -> String {
let values : Array[Json] = []
for contact in contacts {
values.push(Json::string(contact))
}
@json.dumps(
Json::object({
"contact": Json::array(values),
"termsOfServiceAgreed": Json::boolean(terms_agreed),
}),
sort=true,
)
}