///|
/// Raw feature scan for the support-validation stage.
///
/// `Han-Wentao/mooncontract` currently drops several OpenAPI keywords without a
/// diagnostic (`oneOf`, `anyOf`, `allOf`, `discriminator`, `xml`, `not`,
/// `callbacks`, `webhooks`). Dropping them silently would let the generator emit
/// wire behavior that the contract does not describe, so the frontend records
/// their locations in the raw document instead of re-implementing a parser.
///
/// The scan records *locations only*. Classification into
/// supported/fallback/unsupported belongs to the generator's support validator.
let unsupported_keywords : Array[String] = [
"oneOf", "anyOf", "allOf", "discriminator", "xml", "not", "callbacks", "webhooks",
]
///|
/// Keywords that MoonContract parses into fields this frontend does not carry
/// into the Frontend Model. They are recorded so that support validation can
/// report them as explicit warnings instead of silently dropping them.
let ignored_keywords : Array[String] = [
"default", "example", "examples", "minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum",
"minLength", "maxLength", "pattern", "minItems", "maxItems", "uniqueItems", "readOnly",
"writeOnly", "deprecated", "additionalProperties",
]
///|
/// Scalar rendering of a keyword value, when it has one.
fn keyword_value(keyword : String, json : Json) -> Json {
if keyword == "additionalProperties" {
match json {
Object(_) => Json::string("schema")
_ => Json::null()
}
} else {
match json {
String(text) => Json::string(text)
True => Json::string("true")
False => Json::string("false")
_ => Json::null()
}
}
}
///|
/// RFC 6901 JSON Pointer token escaping.
fn pointer_escape(value : String) -> String {
value.replace_all(old="~", new="~0").replace_all(old="/", new="~1")
}
///|
/// Scan only schema-bearing nodes and known operation-level feature slots.
///
/// This is intentionally a bounded sidecar, not a second OpenAPI parser. In
/// particular it never descends into `example`, `default`, `description`, or
/// arbitrary extension values. A user may therefore legitimately use a key
/// such as `oneOf` inside example data without producing an unsupported-schema
/// diagnostic.
fn scan_schema(
node : Json,
pointer : String,
out : Array[Json],
keywords : Array[String],
) -> Unit {
match node {
Object(entries) => {
for keyword in keywords {
match entries.get(keyword) {
Some(value) => {
let record = if keyword == "additionalProperties" {
match value {
Object(_) => true
_ => false
}
} else {
true
}
if record {
out.push(
Json::object(
Map([
("pointer", Json::string(pointer + "/" + keyword)),
("keyword", Json::string(keyword)),
("value", keyword_value(keyword, value)),
]),
),
)
}
}
None => ()
}
}
match entries.get("properties") {
Some(properties) =>
match properties {
Object(children) =>
for name, child in children {
scan_schema(
child,
pointer + "/properties/" + pointer_escape(name),
out,
keywords,
)
}
_ => ()
}
None => ()
}
match entries.get("items") {
Some(items) => scan_schema(items, pointer + "/items", out, keywords)
None => ()
}
match entries.get("additionalProperties") {
Some(value) =>
match value {
Object(_) =>
scan_schema(
value,
pointer + "/additionalProperties",
out,
keywords,
)
_ => ()
}
None => ()
}
}
_ => ()
}
}
///|
fn scan_content(
node : Json,
pointer : String,
out : Array[Json],
keywords : Array[String],
) -> Unit {
match node {
Object(entries) =>
match entries.get("schema") {
Some(schema) => scan_schema(schema, pointer + "/schema", out, keywords)
None => ()
}
_ => ()
}
}
///|
/// Scan schemas attached to a parameter array. This is shared by operation
/// parameters and path-item inherited parameters; the latter are easy to miss
/// because mooncontract merges them into each operation.
fn scan_parameters(
node : Json,
pointer : String,
out : Array[Json],
keywords : Array[String],
) -> Unit {
match node {
Array(values) =>
for index, parameter in values {
match parameter {
Object(parameter_entries) =>
match parameter_entries.get("schema") {
Some(schema) =>
scan_schema(
schema,
pointer + "/" + index.to_string() + "/schema",
out,
keywords,
)
None => ()
}
_ => ()
}
}
_ => ()
}
}
///|
fn scan_operation(
node : Json,
pointer : String,
out : Array[Json],
keywords : Array[String],
) -> Unit {
match node {
Object(entries) => {
for keyword in ["callbacks", "webhooks"] {
match entries.get(keyword) {
Some(value) => {
let record = if keyword == "additionalProperties" {
match value {
Object(_) => true
_ => false
}
} else {
true
}
if record {
out.push(
Json::object(
Map([
("pointer", Json::string(pointer + "/" + keyword)),
("keyword", Json::string(keyword)),
("value", keyword_value(keyword, value)),
]),
),
)
}
}
None => ()
}
}
match entries.get("parameters") {
Some(parameters) =>
scan_parameters(parameters, pointer + "/parameters", out, keywords)
None => ()
}
match entries.get("requestBody") {
Some(body) =>
match body {
Object(body_entries) =>
match body_entries.get("content") {
Some(content) =>
match content {
Object(media_types) =>
for media_type, value in media_types {
scan_content(
value,
pointer +
"/requestBody/content/" +
pointer_escape(media_type),
out,
keywords,
)
}
_ => ()
}
None => ()
}
_ => ()
}
None => ()
}
match entries.get("responses") {
Some(responses) =>
match responses {
Object(response_map) =>
for status, response in response_map {
match response {
Object(response_entries) =>
match response_entries.get("content") {
Some(content) =>
match content {
Object(media_types) =>
for media_type, value in media_types {
scan_content(
value,
pointer +
"/responses/" +
pointer_escape(status) +
"/content/" +
pointer_escape(media_type),
out,
keywords,
)
}
_ => ()
}
None => ()
}
_ => ()
}
}
_ => ()
}
None => ()
}
}
_ => ()
}
}
///|
fn raw_keyword_findings(root : Json, keywords : Array[String]) -> Json {
let out : Array[Json] = []
match raw_get(root, "components") {
Object(components) =>
match components.get("schemas") {
Some(schemas) =>
match schemas {
Object(entries) =>
for name, schema in entries {
scan_schema(
schema,
"#/components/schemas/" + pointer_escape(name),
out,
keywords,
)
}
_ => ()
}
None => ()
}
_ => ()
}
match raw_get(root, "paths") {
Object(paths) =>
for path, path_item in paths {
match path_item {
Object(methods) => {
match methods.get("parameters") {
Some(parameters) =>
scan_parameters(
parameters,
"#/paths/" + pointer_escape(path) + "/parameters",
out,
keywords,
)
None => ()
}
for verb, operation in methods {
if [
"get", "post", "put", "patch", "delete", "head", "options", "trace",
].contains(verb) {
scan_operation(
operation,
"#/paths/" + pointer_escape(path) + "/" + pointer_escape(verb),
out,
keywords,
)
}
}
}
_ => ()
}
}
_ => ()
}
Json::array(out)
}
///|
/// The recorded unsupported-keyword locations, in document order.
fn raw_unsupported_issues(root : Json) -> Json {
raw_keyword_findings(root, unsupported_keywords)
}
///|
/// Keywords that are dropped by the frontend, recorded for reporting only.
fn raw_ignored_issues(root : Json) -> Json {
raw_keyword_findings(root, ignored_keywords)
}
///|
/// Raw serialization metadata for one operation parameter.
///
/// OpenAPI defaults differ per location, so the raw `style`/`explode` are
/// carried through as declared rather than applied here.
fn raw_parameter_serialization(
root : Json,
path : String,
http_method : String,
index : Int,
) -> (Json, Json) {
let path_parameters = raw_array(
raw_get(raw_get(raw_get(root, "paths"), path), "parameters"),
)
let operation_parameters = raw_array(
raw_get(
raw_get(raw_get(raw_get(root, "paths"), path), http_method.to_lower()),
"parameters",
),
)
let parameter = if index < path_parameters.length() {
match path_parameters.get(index) {
Some(value) => value
None => Json::null()
}
} else {
match operation_parameters.get(index - path_parameters.length()) {
Some(value) => value
None => Json::null()
}
}
let style = match raw_as_string(raw_get(parameter, "style")) {
Some(text) => Json::string(text)
None => Json::null()
}
let explode = match raw_boolean(raw_get(parameter, "explode")) {
Some(value) => Json::boolean(value)
None => Json::null()
}
(style, explode)
}