///|
priv struct Operation {
ctx : Context
value : Node
path_item : Node
parameters : Map[String, Node]
body : Node?
body_object : Node?
}
///|
fn http_methods() -> Array[String] {
["delete", "get", "head", "options", "patch", "post", "put", "trace"]
}
///|
fn merge_parameters(
ctx : Context,
parent : Node,
result : Map[String, Node],
) -> Unit {
guard field(parent.raw, "parameters") is Some(raw) else { return }
let collection = parent.child("parameters", raw)
guard raw is Array(items) else {
ctx.diagnose(
"invalid-parameters", "error", collection, "parameters must be an array.",
)
return
}
let seen : Array[String] = []
for index, item in items {
guard ctx.resolve(collection.child(index.to_string(), item)) is Some(value) else {
continue
}
validate_keys(ctx, value, [
"name", "in", "description", "required", "deprecated", "allowEmptyValue", "style",
"explode", "allowReserved", "schema", "example", "examples", "content",
])
let name = string_value(field(value.raw, "name"))
let place = string_value(field(value.raw, "in"))
guard name is Some(name) && place is Some(place) else {
ctx.diagnose(
"invalid-parameter", "error", value, "Parameters require string name and in.",
)
continue
}
// OAS 3.0.3 Parameter Object: these header definitions SHALL be ignored.
if place == "header" &&
["Accept", "Content-Type", "Authorization"].contains(name) {
continue
}
if !["query", "header", "path", "cookie"].contains(place) {
ctx.diagnose(
"invalid-parameter",
"error",
value,
"Unknown parameter location: " + place,
)
}
validate_bool(ctx, value, "required")
if place == "path" && !is_true(field(value.raw, "required")) {
ctx.diagnose(
"invalid-parameter", "error", value, "Path parameters must be required.",
)
}
let key = place + ":" + name
if seen.contains(key) {
ctx.diagnose(
"duplicate-parameter",
"error",
value,
"Duplicate parameter within one level: " + key,
)
}
seen.push(key)
result[key] = value
}
}
///|
fn request_schema(ctx : Context, operation : Node) -> (Node?, Node?) {
guard field(operation.raw, "requestBody") is Some(raw) else {
return (None, None)
}
guard ctx.resolve(operation.child("requestBody", raw)) is Some(body) else {
return (None, None)
}
validate_keys(ctx, body, ["description", "required", "content"])
validate_bool(ctx, body, "required")
guard field(body.raw, "content") is Some(Object(media)) else {
ctx.diagnose(
"invalid-request-body", "error", body, "requestBody.content must be an object.",
)
return (None, Some(body))
}
if media.is_empty() {
ctx.diagnose(
"invalid-request-body", "error", body, "requestBody.content must not be empty.",
)
}
for name, _ in media {
if name != "application/json" {
ctx.diagnose(
"unsupported-media-type",
"unsupported",
body,
"V1 supports only application/json request bodies: " + name,
)
}
}
guard media.get("application/json") is Some(json_media) else {
return (None, Some(body))
}
let content_node = body
.child("content", Json::object(media))
.child("application/json", json_media)
validate_keys(ctx, content_node, ["schema", "example", "examples", "encoding"])
if field(json_media, "encoding") is Some(_) {
ctx.diagnose(
"unsupported-encoding", "unsupported", content_node, "Media encoding is outside V1.",
)
}
guard field(json_media, "schema") is Some(schema) else {
ctx.diagnose(
"invalid-request-body", "error", content_node, "The JSON media type needs a schema.",
)
return (None, Some(body))
}
let schema_node = content_node.child("schema", schema)
if ctx.resolve(schema_node) is Some(resolved) {
if string_value(field(resolved.raw, "type")) != Some("object") {
ctx.diagnose(
"unsupported-request-body", "unsupported", schema_node, "V1 request bodies must be JSON objects.",
)
}
}
(Some(schema_node), Some(body))
}
///|
fn collect_operations(
root : Json,
side : String,
diagnostics : Array[Diagnostic],
budget : WorkBudget,
) -> Map[String, Operation] {
let operations : Map[String, Operation] = Map([])
let paths = field(root, "paths").unwrap_or(Json::empty_object())
for path in keys(paths) {
if path.has_prefix("x-") {
continue
}
let path_ctx : Context = {
root,
side,
http_method: "",
path,
diagnostics,
budget,
}
let path_node = node(
field(paths, path).unwrap(),
"/paths/" + pointer_token(path),
)
if !path.has_prefix("/") {
path_ctx.diagnose(
"invalid-path", "error", path_node, "Path keys must begin with /.",
)
}
guard path_ctx.resolve(path_node) is Some(path_item) else { continue }
guard path_item.raw is Object(_) else {
path_ctx.diagnose(
"invalid-path-item", "error", path_item, "Path items must be objects.",
)
continue
}
validate_keys(path_ctx, path_item, [
"summary", "description", "servers", "parameters", "delete", "get", "head",
"options", "patch", "post", "put", "trace",
])
for verb in http_methods() {
guard field(path_item.raw, verb) is Some(raw_operation) else { continue }
let ctx : Context = {
root,
side,
http_method: verb.to_upper(),
path,
diagnostics,
budget,
}
let value = path_item.child(verb, raw_operation)
guard raw_operation is Object(_) else {
ctx.diagnose(
"invalid-operation", "error", value, "Operations must be objects.",
)
continue
}
validate_keys(ctx, value, [
"tags", "summary", "description", "externalDocs", "operationId", "parameters",
"requestBody", "responses", "callbacks", "deprecated", "security", "servers",
])
match field(raw_operation, "responses") {
Some(Object(responses)) =>
if responses.is_empty() {
ctx.diagnose(
"invalid-responses", "error", value, "responses must not be empty.",
)
}
_ =>
ctx.diagnose(
"invalid-responses", "error", value, "An operation requires a responses object.",
)
}
if field(raw_operation, "callbacks") is Some(_) {
ctx.diagnose(
"unsupported-callbacks", "unsupported", value, "Callback contracts are outside V1.",
)
}
let parameters : Map[String, Node] = Map([])
merge_parameters(ctx, path_item, parameters)
merge_parameters(ctx, value, parameters)
let parameter_keys = parameters.keys().to_array()
parameter_keys.sort()
for key in parameter_keys {
let parameter = parameters[key]
if string_value(field(parameter.raw, "in")) == Some("path") {
let name = string_value(field(parameter.raw, "name")).unwrap_or("")
if !path.contains("{" + name + "}") {
ctx.diagnose(
"invalid-path-parameter",
"error",
parameter,
"Path parameter has no matching template expression: " + name,
)
}
}
if field(parameter.raw, "content") is Some(_) {
ctx.diagnose(
"unsupported-parameter", "unsupported", parameter, "Parameter content is outside V1.",
)
}
match field(parameter.raw, "schema") {
Some(schema) => {
let schema_node = parameter.child("schema", schema)
scan_schema(ctx, schema_node, 0)
if ctx.resolve(schema_node) is Some(resolved) {
if ![
Some("string"),
Some("integer"),
Some("number"),
Some("boolean"),
].contains(string_value(field(resolved.raw, "type"))) {
ctx.diagnose(
"unsupported-parameter", "unsupported", schema_node, "V1 parameters must be scalar.",
)
}
}
}
None =>
ctx.diagnose(
"unsupported-parameter", "unsupported", parameter, "V1 parameters require schema.",
)
}
}
let (body, body_object) = request_schema(ctx, value)
if field(value.raw, "responses") is Some(responses) {
ignore(
resolved_response(ctx, value.child("responses", responses), true, 0),
)
}
if body is Some(schema) {
scan_schema(ctx, schema, 0)
}
operations[path + " " + verb] = {
ctx,
value,
path_item,
parameters,
body,
body_object,
}
}
}
operations
}