///|
fn operations(document : Value) -> Map[String, Value] raise {
let version = str(get(document, "openapi"))
if !version.has_prefix("3.0.") {
raise InputError("this profile supports OpenAPI 3.0 JSON")
}
let result : Map[String, Value] = Map([])
for path, item in obj(get(document, "paths")) {
if !path.has_prefix("/") {
raise InputError("operation path must be absolute")
}
for
method in [
"get", "post", "put", "patch", "delete", "head", "options", "trace",
] {
if get(item, method) != Null {
let op = obj(get(item, method)).copy()
let parameters = if get(item, "parameters") == Null {
[]
} else {
arr(get(item, "parameters"))
}
let overrides = if get(Object(op), "parameters") == Null {
[]
} else {
arr(op["parameters"])
}
let merged : Map[String, Value] = Map([])
for raw in parameters + overrides {
let parameter = if get(raw, "$ref") == Null {
raw
} else {
pointer(document, str(get(raw, "$ref")))
}
merged[str(get(parameter, "in")) + ":" + str(get(parameter, "name"))] = parameter
}
op["parameters"] = Array(merged.values().collect())
result[method + " " + path] = Object(op)
}
}
}
result
}
///|
fn json_body(
state : Validation,
definition : Value,
value : Value,
path : String,
) -> Unit raise {
let media = get(get(definition, "content"), "application/json")
if media == Null {
if get(definition, "content") != Null {
state.unchecked.push(
record([("path", String(path)), ("keyword", String("non-JSON media"))]),
)
}
return
}
if get(media, "schema") != Null {
state.schema(get(media, "schema"), value, path, 0)
}
}
///|
pub fn verify_case(document : Value, case : Value) -> Value raise {
let key = str(get(case, "method")).to_lower() + " " + str(get(case, "path"))
let operation = match operations(document).get(key) {
Some(op) => op
None => raise InputError("operation not found: " + key)
}
let state = Validation::{
document,
issues: [],
unchecked: [],
remaining: 100000,
}
for parameter in arr(get(operation, "parameters")) {
let name = str(get(parameter, "name"))
let location = str(get(parameter, "in"))
let values = get(
case,
if location == "path" {
"path_parameters"
} else {
location
},
)
let present = match values {
Object(fields) => fields.contains(name)
_ => false
}
if !present {
if get(parameter, "required") == Bool(true) || location == "path" {
state.issue("request/" + location + "/" + name, "required")
}
} else if get(parameter, "schema") != Null {
state.schema(
get(parameter, "schema"),
get(values, name),
"request/" + location + "/" + name,
0,
)
}
}
let body = get(operation, "requestBody")
if body != Null {
let definition = if get(body, "$ref") == Null {
body
} else {
pointer(document, str(get(body, "$ref")))
}
if !obj(case).contains("body") {
if get(definition, "required") == Bool(true) {
state.issue("request/body", "required")
}
} else {
json_body(state, definition, get(case, "body"), "request/body")
}
}
let response = get(case, "response")
if response != Null {
let status = num(get(response, "status"))
if status.floor() != status || status < 100.0 || status > 599.0 {
raise InputError("invalid HTTP status")
}
let responses = obj(get(operation, "responses"))
let definition = responses
.get(status.to_int().to_string())
.unwrap_or(responses.get("default").unwrap_or(Null))
if definition == Null {
state.issue("response/status", "undeclared status")
} else {
let definition = if get(definition, "$ref") == Null {
definition
} else {
pointer(document, str(get(definition, "$ref")))
}
if get(definition, "headers") != Null {
state.unchecked.push(
record([
("path", String("response/headers")),
("keyword", String("response header constraints")),
]),
)
}
if status == 204.0 && get(response, "body") != Null {
state.issue("response/body", "204 must have no body")
} else {
json_body(state, definition, get(response, "body"), "response/body")
}
}
}
record([
("operation", String(key)),
("valid", Bool(state.issues.is_empty())),
("complete", Bool(state.unchecked.is_empty())),
("issues", Array(state.issues)),
("unchecked", Array(state.unchecked)),
])
}
///|
fn compatibility(old : Value, new : Value) -> Value raise {
let before = operations(old)
let after = operations(new)
let changes : Array[Value] = []
let keys = before.keys().collect()
keys.sort()
for key in keys {
let a = before[key]
match after.get(key) {
None =>
changes.push(
record([
("operation", String(key)),
("classification", String("breaking")),
("reason", String("operation removed")),
]),
)
Some(b) =>
if a != b {
let old_parameters : Map[String, Value] = Map([])
for p in arr(get(a, "parameters")) {
old_parameters[str(get(p, "in")) + ":" + str(get(p, "name"))] = p
}
let mut breaking = false
for p in arr(get(b, "parameters")) {
let id = str(get(p, "in")) + ":" + str(get(p, "name"))
if get(p, "required") == Bool(true) &&
get(old_parameters.get(id).unwrap_or(Null), "required") !=
Bool(true) {
breaking = true
}
}
if get(get(b, "requestBody"), "required") == Bool(true) &&
get(get(a, "requestBody"), "required") != Bool(true) {
breaking = true
}
changes.push(
record([
("operation", String(key)),
(
"classification",
String(if breaking { "breaking" } else { "review" }),
),
(
"reason",
String(
if breaking {
"new required request input"
} else {
"changed schema or operation requires semantic review"
},
),
),
]),
)
}
}
}
let added = after.keys().filter(fn(key) { !before.contains(key) }).collect()
added.sort()
for key in added {
changes.push(
record([
("operation", String(key)),
("classification", String("additive")),
("reason", String("operation added")),
]),
)
}
record([
("changes", Array(changes)),
(
"breaking",
Bool(
changes.any(fn(c) { get(c, "classification") == String("breaking") }),
),
),
(
"needs_review",
Bool(changes.any(fn(c) { get(c, "classification") == String("review") })),
),
])
}
///|
pub fn run(request : Value) -> Value raise {
match optional_str(get(request, "operation"), "verify") {
"schema" =>
validate_schema(
get(request, "document"),
get(request, "schema"),
get(request, "value"),
)
"diff" => compatibility(get(request, "old"), get(request, "new"))
"verify" => {
let document = get(request, "document")
let cases = arr(get(request, "cases"))
let results = cases.map(fn(case) { verify_case(document, case) })
record([
("results", Array(results)),
("valid", Bool(results.all(fn(r) { get(r, "valid") == Bool(true) }))),
(
"complete",
Bool(results.all(fn(r) { get(r, "complete") == Bool(true) })),
),
])
}
_ => raise InputError("unknown contract operation")
}
}