Rhys Sullivantrim sizes71e33a97 months agoHistory
1016 lines · 30.5 KB · Rust
RawDownload
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}; use serde::Serialize;use serde_json::{Map, Value, json};use sha2::{Digest, Sha256}; #[cfg(target_arch = "wasm32")]use wasm_bindgen::prelude::*; const OPEN_API_HTTP_METHODS: [&str; 8] = [    "get",    "put",    "post",    "delete",    "patch",    "head",    "options",    "trace",]; const OPEN_API_PARAMETER_LOCATIONS: [&str; 4] = ["path", "query", "header", "cookie"]; #[derive(Serialize, Clone)]struct OpenApiToolParameter {    name: String,    location: String,    required: bool,} #[derive(Serialize, Clone)]#[serde(rename_all = "camelCase")]struct OpenApiToolRequestBody {    required: bool,    content_types: Vec<String>,} #[derive(Serialize, Clone)]#[serde(rename_all = "camelCase")]struct OpenApiInvocationPayload {    method: String,    path_template: String,    parameters: Vec<OpenApiToolParameter>,    request_body: Option<OpenApiToolRequestBody>,} #[derive(Serialize, Clone)]#[serde(rename_all = "camelCase")]struct DiscoveryTypingPayload {    #[serde(skip_serializing_if = "Option::is_none")]    input_schema: Option<Value>,    #[serde(skip_serializing_if = "Option::is_none")]    output_schema: Option<Value>,    #[serde(skip_serializing_if = "Option::is_none")]    ref_hint_keys: Option<Vec<String>>,} #[derive(Serialize, Clone)]#[serde(rename_all = "camelCase")]struct OpenApiExample {    value_json: String,    #[serde(skip_serializing_if = "Option::is_none")]    media_type: Option<String>,    #[serde(skip_serializing_if = "Option::is_none")]    label: Option<String>,} #[derive(Serialize, Clone)]#[serde(rename_all = "camelCase")]struct OpenApiParameterDocumentation {    name: String,    location: String,    required: bool,    #[serde(skip_serializing_if = "Option::is_none")]    description: Option<String>,    #[serde(skip_serializing_if = "Option::is_none")]    examples: Option<Vec<OpenApiExample>>,} #[derive(Serialize, Clone)]#[serde(rename_all = "camelCase")]struct OpenApiRequestBodyDocumentation {    #[serde(skip_serializing_if = "Option::is_none")]    description: Option<String>,    #[serde(skip_serializing_if = "Option::is_none")]    examples: Option<Vec<OpenApiExample>>,} #[derive(Serialize, Clone)]#[serde(rename_all = "camelCase")]struct OpenApiResponseDocumentation {    status_code: String,    #[serde(skip_serializing_if = "Option::is_none")]    description: Option<String>,    content_types: Vec<String>,    #[serde(skip_serializing_if = "Option::is_none")]    examples: Option<Vec<OpenApiExample>>,} #[derive(Serialize, Clone)]#[serde(rename_all = "camelCase")]struct OpenApiToolDocumentation {    #[serde(skip_serializing_if = "Option::is_none")]    summary: Option<String>,    #[serde(skip_serializing_if = "Option::is_none")]    deprecated: Option<bool>,    parameters: Vec<OpenApiParameterDocumentation>,    #[serde(skip_serializing_if = "Option::is_none")]    request_body: Option<OpenApiRequestBodyDocumentation>,    #[serde(skip_serializing_if = "Option::is_none")]    response: Option<OpenApiResponseDocumentation>,} #[derive(Serialize, Clone)]#[serde(rename_all = "camelCase")]struct OpenApiExtractedTool {    tool_id: String,    #[serde(skip_serializing_if = "Option::is_none")]    operation_id: Option<String>,    tags: Vec<String>,    name: String,    description: Option<String>,    method: String,    path: String,    invocation: OpenApiInvocationPayload,    operation_hash: String,    #[serde(skip_serializing_if = "Option::is_none")]    typing: Option<DiscoveryTypingPayload>,    #[serde(skip_serializing_if = "Option::is_none")]    documentation: Option<OpenApiToolDocumentation>,} #[derive(Serialize)]#[serde(rename_all = "camelCase")]struct OpenApiToolManifest {    version: u8,    source_hash: String,    tools: Vec<OpenApiExtractedTool>,    #[serde(skip_serializing_if = "Option::is_none")]    ref_hint_table: Option<BTreeMap<String, String>>,} pub fn extract_manifest_json(    source_name: &str,    openapi_document_text: &str,    pretty: bool,) -> Result<String, String> {    let spec = parse_openapi_document(openapi_document_text)?;    let manifest = extract_openapi_manifest(source_name, &spec)?;     if pretty {        serde_json::to_string_pretty(&manifest).map_err(|error| format!("serialize manifest: {error}"))    } else {        serde_json::to_string(&manifest).map_err(|error| format!("serialize manifest: {error}"))    }} #[cfg(target_arch = "wasm32")]#[wasm_bindgen]pub fn extract_manifest_json_wasm(    source_name: &str,    openapi_document_text: &str,) -> Result<String, JsValue> {    extract_manifest_json(source_name, openapi_document_text, false)        .map_err(|error| JsValue::from_str(&error))} fn parse_openapi_document(input: &str) -> Result<Value, String> {    let text = input.trim();    if text.is_empty() {        return Err("OpenAPI document is empty".to_string());    }     match serde_json::from_str::<Value>(text) {        Ok(value) => Ok(value),        Err(_) => serde_yaml::from_str::<Value>(text)            .map_err(|error| format!("Unable to parse OpenAPI document as JSON or YAML: {error}")),    }} fn as_object(value: &Value) -> Option<&Map<String, Value>> {    value.as_object()} fn as_trimmed_string(value: Option<&Value>) -> Option<String> {    let candidate = value?.as_str()?.trim();    if candidate.is_empty() {        return None;    }     Some(candidate.to_string())} fn response_status_rank(status_code: &str) -> i32 {    if status_code.len() == 3        && status_code.starts_with('2')        && status_code.chars().all(|character| character.is_ascii_digit())    {        return 0;    }     if status_code == "default" {        return 1;    }     2} fn to_stable_value(value: &Value) -> Value {    match value {        Value::Array(items) => Value::Array(items.iter().map(to_stable_value).collect()),        Value::Object(object) => {            let mut keys: Vec<&String> = object.keys().collect();            keys.sort();             let mut stable = Map::new();            for key in keys {                if let Some(item) = object.get(key) {                    stable.insert(key.clone(), to_stable_value(item));                }            }             Value::Object(stable)        }        Value::Number(number) => {            if let Some(float_value) = number.as_f64()                && float_value.is_finite()                && float_value.fract() == 0.0            {                if float_value >= 0.0 && float_value <= u64::MAX as f64 {                    let unsigned = float_value as u64;                    if unsigned as f64 == float_value {                        return Value::Number(serde_json::Number::from(unsigned));                    }                }                 if float_value >= i64::MIN as f64 && float_value <= i64::MAX as f64 {                    let signed = float_value as i64;                    if signed as f64 == float_value {                        return Value::Number(serde_json::Number::from(signed));                    }                }            }             Value::Number(number.clone())        }        _ => value.clone(),    }} fn hash_unknown(value: &Value) -> Result<String, String> {    let stable_json =        serde_json::to_string(&to_stable_value(value)).map_err(|error| format!("hash json: {error}"))?;     let mut hasher = Sha256::new();    hasher.update(stable_json.as_bytes());    Ok(format!("{:x}", hasher.finalize()))} fn encode_stable_json(value: &Value) -> Result<String, String> {    serde_json::to_string(&to_stable_value(value)).map_err(|error| format!("encode stable json: {error}"))} fn to_extracted_tool_parameter_from_record(record: &Map<String, Value>) -> Option<OpenApiToolParameter> {    let name = record.get("name")?.as_str()?.trim();     if name.is_empty() {        return None;    }     let location = record.get("in")?.as_str()?;    if !OPEN_API_PARAMETER_LOCATIONS.contains(&location) {        return None;    }     let required = location == "path" || record.get("required").and_then(Value::as_bool) == Some(true);     Some(OpenApiToolParameter {        name: name.to_string(),        location: location.to_string(),        required,    })} fn to_extracted_tool_parameter(root: &Value, value: &Value) -> Option<OpenApiToolParameter> {    let record = resolve_local_reference_object(root, value)?;    to_extracted_tool_parameter_from_record(&record)} fn merge_parameters(root: &Value, path_item: &Map<String, Value>, operation: &Map<String, Value>) -> Vec<OpenApiToolParameter> {    let mut merged: Vec<OpenApiToolParameter> = collect_parameter_record_by_key(root, path_item, operation)        .into_values()        .filter_map(|record| to_extracted_tool_parameter_from_record(&record))        .collect();    merged.sort_by(|left, right| {        left.location            .cmp(&right.location)            .then_with(|| left.name.cmp(&right.name))    });    merged} fn extract_request_body(root: &Value, operation: &Map<String, Value>) -> Option<OpenApiToolRequestBody> {    let request_body = resolve_request_body_record(root, operation)?;    let content = request_body.get("content").and_then(as_object);     Some(OpenApiToolRequestBody {        required: request_body.get("required").and_then(Value::as_bool) == Some(true),        content_types: collect_content_types(content),    })} fn build_invocation_metadata(    root: &Value,    method: &str,    path_value: &str,    path_item: &Map<String, Value>,    operation: &Map<String, Value>,) -> OpenApiInvocationPayload {    OpenApiInvocationPayload {        method: method.to_string(),        path_template: path_value.to_string(),        parameters: merge_parameters(root, path_item, operation),        request_body: extract_request_body(root, operation),    }} fn collect_ref_keys(value: &Value, refs: &mut BTreeSet<String>) {    match value {        Value::Array(items) => {            for item in items {                collect_ref_keys(item, refs);            }        }        Value::Object(record) => {            if let Some(reference) = record.get("$ref").and_then(Value::as_str)                && reference.starts_with("#/")            {                refs.insert(reference.to_string());            }             for nested in record.values() {                collect_ref_keys(nested, refs);            }        }        _ => {}    }} fn resolve_json_pointer<'a>(root: &'a Value, pointer: &str) -> Option<&'a Value> {    if !pointer.starts_with("#/") {        return None;    }     let mut current = root;    for part in pointer[2..].split('/') {        let decoded = part.replace("~1", "/").replace("~0", "~");        let next = current.as_object()?.get(&decoded)?;        current = next;    }     Some(current)} fn resolve_local_reference_value(    root: &Value,    value: &Value,    active_refs: &HashSet<String>,) -> Value {    let Some(record) = as_object(value) else {        return value.clone();    };     let Some(reference) = record.get("$ref").and_then(Value::as_str) else {        return value.clone();    };     if !reference.starts_with("#/") || active_refs.contains(reference) {        return value.clone();    }     let Some(resolved) = resolve_json_pointer(root, reference) else {        return value.clone();    };     let mut next_active_refs = active_refs.clone();    next_active_refs.insert(reference.to_string());     let mut resolved_value = resolve_local_reference_value(root, resolved, &next_active_refs);     if let Some(resolved_record) = resolved_value.as_object_mut() {        for (key, nested) in record {            if key != "$ref" {                resolved_record.insert(key.clone(), nested.clone());            }        }        return resolved_value;    }     value.clone()} fn resolve_local_reference_object(root: &Value, value: &Value) -> Option<Map<String, Value>> {    as_object(&resolve_local_reference_value(root, value, &HashSet::new())).cloned()} fn collect_parameter_record_by_key(    root: &Value,    path_item: &Map<String, Value>,    operation: &Map<String, Value>,) -> HashMap<String, Map<String, Value>> {    let mut records = HashMap::new();     let mut add_parameters = |candidate: Option<&Value>| {        let Some(items) = candidate.and_then(Value::as_array) else {            return;        };         for item in items {            let Some(record) = resolve_local_reference_object(root, item) else {                continue;            };             let Some(parameter) = to_extracted_tool_parameter_from_record(&record) else {                continue;            };             records.insert(                format!("{}:{}", parameter.location, parameter.name),                record,            );        }    };     add_parameters(path_item.get("parameters"));    add_parameters(operation.get("parameters"));     records} fn resolve_request_body_record(root: &Value, operation: &Map<String, Value>) -> Option<Map<String, Value>> {    resolve_local_reference_object(root, operation.get("requestBody")?)} fn collect_content_types(content: Option<&Map<String, Value>>) -> Vec<String> {    let mut content_types = content        .map(|object| object.keys().cloned().collect::<Vec<String>>())        .unwrap_or_default();    content_types.sort();    content_types} fn pick_content_entry(content: Option<&Value>) -> Option<(String, Map<String, Value>)> {    let content_record = as_object(content?)?;     let mut keys: Vec<String> = content_record.keys().cloned().collect();    keys.sort();     let mut preferred = Vec::with_capacity(keys.len() + 1);    preferred.push("application/json".to_string());    preferred.extend(keys);     let mut seen = HashSet::new();    for media_type in preferred {        if !seen.insert(media_type.clone()) {            continue;        }         let Some(media_type_value) = content_record.get(&media_type).and_then(as_object) else {            continue;        };         return Some((media_type, media_type_value.clone()));    }     None} fn pick_example_values(value: &Value, label: Option<String>, media_type: Option<String>) -> Result<Vec<OpenApiExample>, String> {    let mut examples = Vec::new();    let Some(record) = as_object(value) else {        return Ok(examples);    };     if let Some(example_value) = record.get("example") {        examples.push(OpenApiExample {            value_json: encode_stable_json(example_value)?,            media_type: media_type.clone(),            label: label.clone(),        });    }     if let Some(example_values) = record.get("examples").and_then(Value::as_object) {        let mut keys: Vec<String> = example_values.keys().cloned().collect();        keys.sort();         for key in keys {            let Some(example_entry) = example_values.get(&key) else {                continue;            };            let example_record = as_object(example_entry)

Lines 1–500 of 1016.