///|
fn required(fields : Map[String, Json], key : String) -> Json raise CodaError {
match fields.get(key) {
Some(value) => value
None => raise Invalid("missing required request field")
}
}
///|
fn[T : @json.FromJson] decode(value : Json) -> T raise CodaError {
@json.from_json(value) catch {
_ => raise Invalid("request field has wrong type")
}
}
///|
fn request_fields(
fields : Map[String, Json],
allowed : Array[String],
) -> Unit raise CodaError {
for key, _ in fields {
let mut found = false
for name in allowed {
if key == name {
found = true
}
}
if !found {
raise Invalid("unknown field for selected operation")
}
}
}
///|
fn execute_request(request : Json) -> Json raise CodaError {
guard request is Object(fields) else {
raise Invalid("request must be an object")
}
let op : String = decode(required(fields, "op"))
let allowed = ["op", "features", "samples", "rows"]
match op {
"analyze" => ()
"pca" => allowed.push("components")
"cluster" => {
allowed.push("clusters")
allowed.push("max_iterations")
}
"score" => allowed.push("ridge")
_ => raise Invalid("unknown operation")
}
request_fields(fields, allowed)
let features : Array[String] = decode(required(fields, "features"))
let samples : Array[String] = decode(required(fields, "samples"))
let rows : Array[Array[Double]] = decode(required(fields, "rows"))
let data = dataset(features, samples, rows)
let result = Map([])
result["ok"] = @json.to_json(true)
result["operation"] = @json.to_json(op)
result["features"] = @json.to_json(data.feature_names())
result["samples"] = @json.to_json(data.sample_names())
match op {
"analyze" => {
result["center"] = @json.to_json(center(data))
result["clr"] = @json.to_json(data.clr_rows())
result["ilr"] = @json.to_json(
data.ilr_rows(helmert_basis(data.part_count())),
)
result["variation"] = @json.to_json(variation_matrix(data))
result["total_variance"] = @json.to_json(total_variance(data))
}
"pca" => {
let components : Int = request_int(required(fields, "components"))
let model = pca(data, components)
let scores = model.transform(data)
result["scores"] = @json.to_json(scores)
result["reconstructed"] = @json.to_json(model.reconstruct(scores))
result["eigenvalues"] = @json.to_json(model.eigenvalues())
result["explained_ratio"] = @json.to_json(model.explained_ratio())
result["loadings_ilr"] = @json.to_json(model.loadings())
result["center"] = @json.to_json(model.center())
}
"cluster" => {
let count : Int = request_int(required(fields, "clusters"))
let limit : Int = match fields.get("max_iterations") {
Some(v) => request_int(v)
None => 100
}
let model = kmeans(data, count, max_iterations=limit)
result["assignments"] = @json.to_json(model.assignments())
result["centers"] = @json.to_json(model.centers())
result["counts"] = @json.to_json(model.counts())
result["inertia"] = @json.to_json(model.inertia())
result["iterations"] = @json.to_json(model.iterations())
result["converged"] = @json.to_json(model.converged())
}
"score" => {
let ridge : Double = decode(required(fields, "ridge"))
let model = gaussian(data, ridge)
result["squared_mahalanobis"] = @json.to_json(
model.squared_mahalanobis(data),
)
result["negative_log_density_ilr"] = @json.to_json(
model.negative_log_density(data),
)
result["rank"] = @json.to_json(model.rank(data))
result["ridge"] = @json.to_json(model.ridge())
result["center"] = @json.to_json(model.center())
}
_ => raise Invalid("unknown operation")
}
@json.to_json(result)
}
///|
fn protocol_error(code : String, message : String) -> Json {
@json.to_json({
"ok": @json.to_json(false),
"code": @json.to_json(code),
"message": @json.to_json(message),
})
}
///|
fn nesting_budget(text : String) -> Unit raise CodaError {
let mut depth = 0
let mut quoted = false
let mut escaped = false
for c in text.iter() {
if quoted {
if escaped {
escaped = false
} else if c == '\\' {
escaped = true
} else if c == '"' {
quoted = false
}
} else if c == '"' {
quoted = true
} else if c == '[' || c == '{' {
depth += 1
if depth > 16 {
raise Budget("JSON nesting exceeds 16")
}
} else if c == ']' || c == '}' {
depth -= 1
}
}
}
///|
/// Pure bounded JSON interface. Errors never echo user-provided input or labels.
/// JSON duplicate keys follow the core parser's last-value semantics.
pub fn process_json(text : String) -> String {
let response = if text.length() > 1000000 {
protocol_error("budget", "request exceeds one million code units")
} else {
try {
nesting_budget(text)
let parsed = @json.parse(text) catch {
_ => raise Invalid("invalid JSON or nesting exceeds 16")
}
execute_request(parsed)
} catch {
Invalid(message) => protocol_error("invalid", message)
Shape(message) => protocol_error("shape", message)
Budget(message) => protocol_error("budget", message)
Numerical(message) => protocol_error("numerical", message)
}
}
response.stringify()
}
///|
fn request_int(value : Json) -> Int raise CodaError {
let n : Double = decode(value)
if !(n >= -1000000.0 && n <= 1000000.0) {
raise Invalid("integer outside request range")
}
let result = n.to_int()
if result.to_double() != n {
raise Invalid("fractional integer request field")
}
result
}