use axum::{ Json, extract::{Extension, Path, State, rejection::JsonRejection}, http::{StatusCode, header::CONTENT_TYPE}, response::{IntoResponse, Response}, }; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use shared_kernel::build_prefixed_uuid_id; use spacetime_client::{ EditorAssetCreateRecordInput, EditorAssetDeleteRecordInput, EditorAssetFolderCreateRecordInput, EditorAssetFolderDeleteRecordInput, EditorAssetFolderUpdateRecordInput, EditorAssetUpdateRecordInput, EditorProjectCreateRecordInput, EditorProjectDeleteRecordInput, EditorProjectGetRecordInput, EditorProjectLayoutSaveRecordInput, EditorProjectRenameRecordInput, EditorProjectResourceCreateRecordInput, }; use crate::{ api_response::json_success_body, character_animation_assets::{ generate_editor_character_animation_for_owner, generate_editor_video_for_owner, }, editor_project::{ EDITOR_ASSET_FOLDER_ID_PREFIX, EDITOR_ASSET_ID_PREFIX, EDITOR_PROJECT_DEFAULT_TITLE, EDITOR_PROJECT_ID_PREFIX, EDITOR_RESOURCE_ID_PREFIX, EditorAssetFolderPayload, EditorAssetLibraryPayload, EditorAssetPayload, EditorCanvasViewportPayload, EditorGenerationCaller, EditorIconSpritesheetGenerationRequest, EditorImageEditRequest, EditorImageGenerationRequest, EditorProjectPayload, EditorProjectResourcePayload, EditorUiDesignAssetExtractionRequest, current_utc_micros, edit_editor_image_for_owner, editor_asset_folder_payload_from_record, editor_asset_library_payload_from_record, editor_asset_payload_from_record, editor_project_payload_from_record, editor_project_resource_payload_from_record, extract_editor_ui_design_assets_for_owner, generate_editor_icon_spritesheet_for_owner, generate_editor_image_for_owner, map_editor_project_error, normalize_editor_persisted_media_src, normalize_optional_string, serialize_editor_asset_metadata, serialize_editor_layers, }, external_api_auth::ExternalApiPrincipal, http_error::AppError, request_context::RequestContext, state::AppState, vector_engine_audio_generation::{ generate_editor_background_music_for_owner, generate_editor_sound_effect_for_owner, }, }; const EXTERNAL_EDITOR_PROVIDER: &str = "external-editor-api"; const SCOPE_EDITOR_PROJECT: &str = "editor:project"; const SCOPE_EDITOR_CANVAS: &str = "editor:canvas"; const SCOPE_EDITOR_IMAGE_GENERATE: &str = "editor:image-generate"; const SCOPE_EDITOR_ASSET: &str = "editor:asset"; const OPENAPI_JSON: &str = include_str!("../../../../docs/openapi/genarrative-external-v1.openapi.json"); #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ExternalEditorProjectCreateRequest { title: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ExternalEditorCanvasSaveRequest { viewport: EditorCanvasViewportPayload, layers: Value, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ExternalEditorProjectRenameRequest { title: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ExternalEditorAssetFolderCreateRequest { label: String, sort_order: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ExternalEditorAssetFolderUpdateRequest { label: Option, collapsed: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ExternalEditorAssetCreateRequest { folder_id: String, label: String, image_src: String, object_key: Option, asset_object_id: Option, width: u32, height: u32, source_type: String, prompt: Option, actual_prompt: Option, model: Option, provider: Option, task_id: Option, asset_kind: Option, generation_inputs: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ExternalEditorProjectResourceCreateRequest { image_src: String, object_key: Option, asset_object_id: Option, width: u32, height: u32, source_type: String, prompt: Option, actual_prompt: Option, model: Option, provider: Option, task_id: Option, source_resource_id: Option, asset_kind: Option, generation_inputs: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ExternalEditorAssetUpdateRequest { label: Option, folder_id: Option, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ExternalEditorProjectResponse { project: EditorProjectPayload, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ExternalEditorProjectRecentResponse { project: Option, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ExternalEditorProjectListResponse { projects: Vec, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ExternalEditorProjectDeleteResponse { deleted_project_id: String, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ExternalEditorAssetLibraryResponse { library: EditorAssetLibraryPayload, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ExternalEditorAssetFolderResponse { folder: EditorAssetFolderPayload, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ExternalEditorAssetFolderDeleteResponse { library: EditorAssetLibraryPayload, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ExternalEditorAssetResponse { asset: EditorAssetPayload, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ExternalEditorProjectResourceResponse { resource: EditorProjectResourcePayload, } pub async fn openapi_json() -> Response { ( [(CONTENT_TYPE, "application/json; charset=utf-8")], OPENAPI_JSON, ) .into_response() } pub async fn create_external_editor_project( State(state): State, Extension(request_context): Extension, Extension(principal): Extension, Json(payload): Json, ) -> Result, AppError> { require_scope(&principal, SCOPE_EDITOR_PROJECT)?; let project = state .spacetime_client() .create_editor_project(EditorProjectCreateRecordInput { project_id: build_prefixed_uuid_id(EDITOR_PROJECT_ID_PREFIX), owner_user_id: principal.owner_user_id().to_string(), title: normalize_project_title(payload.title), now_micros: current_utc_micros(), }) .await .map_err(map_editor_project_error)?; Ok(json_success_body( Some(&request_context), ExternalEditorProjectResponse { project: editor_project_payload_from_record(project), }, )) } pub async fn list_external_editor_projects( State(state): State, Extension(request_context): Extension, Extension(principal): Extension, ) -> Result, AppError> { require_scope(&principal, SCOPE_EDITOR_PROJECT)?; let projects = state .spacetime_client() .list_editor_projects(principal.owner_user_id().to_string()) .await .map_err(map_editor_project_error)? .into_iter() .map(editor_project_payload_from_record) .collect(); Ok(json_success_body( Some(&request_context), ExternalEditorProjectListResponse { projects }, )) } pub async fn load_recent_external_editor_project( State(state): State, Extension(request_context): Extension, Extension(principal): Extension, ) -> Result, AppError> { require_scope(&principal, SCOPE_EDITOR_PROJECT)?; let project = state .spacetime_client() .get_recent_editor_project(principal.owner_user_id().to_string()) .await .map_err(map_editor_project_error)? .map(editor_project_payload_from_record); Ok(json_success_body( Some(&request_context), ExternalEditorProjectRecentResponse { project }, )) } pub async fn get_external_editor_project( State(state): State, Path(project_id): Path, Extension(request_context): Extension, Extension(principal): Extension, ) -> Result, AppError> { require_scope(&principal, SCOPE_EDITOR_PROJECT)?; let project = state .spacetime_client() .get_editor_project(EditorProjectGetRecordInput { project_id, owner_user_id: principal.owner_user_id().to_string(), }) .await .map_err(map_editor_project_error)?; Ok(json_success_body( Some(&request_context), ExternalEditorProjectResponse { project: editor_project_payload_from_record(project), }, )) } pub async fn rename_external_editor_project( State(state): State, Path(project_id): Path, Extension(request_context): Extension, Extension(principal): Extension, Json(payload): Json, ) -> Result, AppError> { require_scope(&principal, SCOPE_EDITOR_PROJECT)?; let project = state .spacetime_client() .rename_editor_project(EditorProjectRenameRecordInput { project_id, owner_user_id: principal.owner_user_id().to_string(), title: payload.title, updated_at_micros: current_utc_micros(), }) .await .map_err(map_editor_project_error)?; Ok(json_success_body( Some(&request_context), ExternalEditorProjectResponse { project: editor_project_payload_from_record(project), }, )) } pub async fn delete_external_editor_project( State(state): State, Path(project_id): Path, Extension(request_context): Extension, Extension(principal): Extension, ) -> Result, AppError> { require_scope(&principal, SCOPE_EDITOR_PROJECT)?; let deleted_project_id = state .spacetime_client() .delete_editor_project(EditorProjectDeleteRecordInput { project_id, owner_user_id: principal.owner_user_id().to_string(), }) .await .map_err(map_editor_project_error)?; Ok(json_success_body( Some(&request_context), ExternalEditorProjectDeleteResponse { deleted_project_id }, )) } pub async fn save_external_editor_canvas( State(state): State, Path(project_id): Path, Extension(request_context): Extension, Extension(principal): Extension, Json(payload): Json, ) -> Result, AppError> { require_scope(&principal, SCOPE_EDITOR_CANVAS)?; let project = state .spacetime_client() .save_editor_project_layout(EditorProjectLayoutSaveRecordInput { project_id, owner_user_id: principal.owner_user_id().to_string(), viewport: payload.viewport.into_record(), layers_json: serialize_editor_layers(payload.layers)?, updated_at_micros: current_utc_micros(), }) .await .map_err(map_editor_project_error)?; Ok(json_success_body( Some(&request_context), ExternalEditorProjectResponse { project: editor_project_payload_from_record(project), }, )) } pub async fn get_external_editor_asset_library( State(state): State, Extension(request_context): Extension, Extension(principal): Extension, ) -> Result, AppError> { require_scope(&principal, SCOPE_EDITOR_ASSET)?; let library = state .spacetime_client() .get_editor_asset_library(principal.owner_user_id().to_string(), current_utc_micros()) .await .map_err(map_editor_project_error)?; Ok(json_success_body( Some(&request_context), ExternalEditorAssetLibraryResponse { library: editor_asset_library_payload_from_record(library), }, )) } pub async fn create_external_editor_asset_folder( State(state): State, Extension(request_context): Extension, Extension(principal): Extension, Json(payload): Json, ) -> Result, AppError> { require_scope(&principal, SCOPE_EDITOR_ASSET)?; let folder = state .spacetime_client() .create_editor_asset_folder(EditorAssetFolderCreateRecordInput { folder_id: build_prefixed_uuid_id(EDITOR_ASSET_FOLDER_ID_PREFIX), owner_user_id: principal.owner_user_id().to_string(), label: payload.label, sort_order: payload.sort_order.unwrap_or(100), now_micros: current_utc_micros(), }) .await .map_err(map_editor_project_error)?; Ok(json_success_body( Some(&request_context), ExternalEditorAssetFolderResponse { folder: editor_asset_folder_payload_from_record(folder), }, )) } pub async fn update_external_editor_asset_folder( State(state): State, Path(folder_id): Path, Extension(request_context): Extension, Extension(principal): Extension, Json(payload): Json, ) -> Result, AppError> { require_scope(&principal, SCOPE_EDITOR_ASSET)?; let folder = state .spacetime_client() .update_editor_asset_folder(EditorAssetFolderUpdateRecordInput { folder_id, owner_user_id: principal.owner_user_id().to_string(), label: normalize_optional_string(payload.label), collapsed: payload.collapsed, updated_at_micros: current_utc_micros(), }) .await .map_err(map_editor_project_error)?; Ok(json_success_body( Some(&request_context), ExternalEditorAssetFolderResponse { folder: editor_asset_folder_payload_from_record(folder), }, )) } pub async fn delete_external_editor_asset_folder( State(state): State, Path(folder_id): Path, Extension(request_context): Extension, Extension(principal): Extension, ) -> Result, AppError> { require_scope(&principal, SCOPE_EDITOR_ASSET)?; let library = state .spacetime_client() .delete_editor_asset_folder(EditorAssetFolderDeleteRecordInput { folder_id, owner_user_id: principal.owner_user_id().to_string(), updated_at_micros: current_utc_micros(), }) .await .map_err(map_editor_project_error)?; Ok(json_success_body( Some(&request_context), ExternalEditorAssetFolderDeleteResponse { library: editor_asset_library_payload_from_record(library), }, )) } pub async fn create_external_editor_asset( State(state): State, Extension(request_context): Extension, Extension(principal): Extension, Json(payload): Json, ) -> Result, AppError> { require_scope(&principal, SCOPE_EDITOR_ASSET)?; let generation_inputs_json = serialize_editor_asset_metadata(payload.generation_inputs.clone())?; let object_key = normalize_optional_string(payload.object_key); let image_src = normalize_editor_persisted_media_src(payload.image_src, object_key.as_deref())?; let asset = state .spacetime_client() .create_editor_asset(EditorAssetCreateRecordInput { asset_id: build_prefixed_uuid_id(EDITOR_ASSET_ID_PREFIX), owner_user_id: principal.owner_user_id().to_string(), folder_id: payload.folder_id, label: payload.label, asset_object_id: normalize_optional_string(payload.asset_object_id), image_src, object_key, width: payload.width, height: payload.height, source_type: payload.source_type, prompt: normalize_optional_string(payload.prompt), actual_prompt: normalize_optional_string(payload.actual_prompt), model: normalize_optional_string(payload.model), provider: normalize_optional_string(payload.provider), task_id: normalize_optional_string(payload.task_id), asset_kind: normalize_optional_string(payload.asset_kind), generation_inputs_json, source_resource_id: None, generation_cost_mud_points: 0, now_micros: current_utc_micros(), thumbnail_src: None, }) .await .map_err(map_editor_project_error)?; Ok(json_success_body( Some(&request_context), ExternalEditorAssetResponse { asset: editor_asset_payload_from_record(asset), }, )) } pub async fn update_external_editor_asset( State(state): State, Path(asset_id): Path, Extension(request_context): Extension, Extension(principal): Extension, Json(payload): Json, ) -> Result, AppError> { require_scope(&principal, SCOPE_EDITOR_ASSET)?; let asset = state .spacetime_client() .update_editor_asset(EditorAssetUpdateRecordInput { asset_id, owner_user_id: principal.owner_user_id().to_string(), label: normalize_optional_string(payload.label), folder_id: normalize_optional_string(payload.folder_id), updated_at_micros: current_utc_micros(), }) .await .map_err(map_editor_project_error)?; Ok(json_success_body( Some(&request_context), ExternalEditorAssetResponse { asset: editor_asset_payload_from_record(asset), }, )) } pub async fn delete_external_editor_asset( State(state): State, Path(asset_id): Path, Extension(request_context): Extension, Extension(principal): Extension, ) -> Result, AppError> { require_scope(&principal, SCOPE_EDITOR_ASSET)?; let asset = state .spacetime_client() .delete_editor_asset(EditorAssetDeleteRecordInput { asset_id, owner_user_id: principal.owner_user_id().to_string(), }) .await .map_err(map_editor_project_error)?; Ok(json_success_body( Some(&request_context), ExternalEditorAssetResponse { asset: editor_asset_payload_from_record(asset), }, )) } pub async fn create_external_editor_project_resource( State(state): State, Path(project_id): Path, Extension(request_context): Extension, Extension(principal): Extension, Json(payload): Json, ) -> Result, AppError> { require_scope(&principal, SCOPE_EDITOR_ASSET)?; let generation_inputs_json = serialize_editor_asset_metadata(payload.generation_inputs.clone())?; let object_key = normalize_optional_string(payload.object_key); let image_src = normalize_editor_persisted_media_src(payload.image_src, object_key.as_deref())?; let resource = state .spacetime_client() .create_editor_project_resource(EditorProjectResourceCreateRecordInput { resource_id: build_prefixed_uuid_id(EDITOR_RESOURCE_ID_PREFIX), project_id, owner_user_id: principal.owner_user_id().to_string(), asset_object_id: normalize_optional_string(payload.asset_object_id), image_src, object_key, width: payload.width, height: payload.height, source_type: payload.source_type, prompt: normalize_optional_string(payload.prompt), actual_prompt: normalize_optional_string(payload.actual_prompt), model: normalize_optional_string(payload.model), provider: normalize_optional_string(payload.provider), task_id: normalize_optional_string(payload.task_id), source_resource_id: normalize_optional_string(payload.source_resource_id), asset_kind: normalize_optional_string(payload.asset_kind), generation_inputs_json, updated_at_micros: current_utc_micros(), }) .await .map_err(map_editor_project_error)?; Ok(json_success_body( Some(&request_context), ExternalEditorProjectResourceResponse { resource: editor_project_resource_payload_from_record(resource), }, )) } pub async fn generate_external_editor_image( State(state): State, Extension(request_context): Extension, Extension(principal): Extension, Json(payload): Json, ) -> Result, AppError> { require_scope(&principal, SCOPE_EDITOR_IMAGE_GENERATE)?; generate_editor_image_for_owner( &state, &request_context, editor_generation_caller(&principal, payload.project_id.clone()), payload, ) .await } pub async fn edit_external_editor_image( State(state): State, Extension(request_context): Extension, Extension(principal): Extension, Json(payload): Json, ) -> Result, AppError> { require_scope(&principal, SCOPE_EDITOR_IMAGE_GENERATE)?; edit_editor_image_for_owner( &state, &request_context, editor_generation_caller(&principal, payload.project_id.clone()), payload, ) .await } pub async fn generate_external_editor_icon_spritesheet( State(state): State, Extension(request_context): Extension, Extension(principal): Extension, Json(payload): Json, ) -> Result, AppError> { require_scope(&principal, SCOPE_EDITOR_IMAGE_GENERATE)?; generate_editor_icon_spritesheet_for_owner( &state, &request_context, editor_generation_caller(&principal, payload.project_id.clone()), payload, ) .await } pub async fn extract_external_editor_ui_design_assets( State(state): State, Extension(request_context): Extension, Extension(principal): Extension, Json(payload): Json, ) -> Result, AppError> { require_scope(&principal, SCOPE_EDITOR_IMAGE_GENERATE)?; extract_editor_ui_design_assets_for_owner( &state, &request_context, editor_generation_caller(&principal, payload.project_id.clone()), payload, ) .await } pub async fn generate_external_editor_character_animation( State(state): State, Extension(request_context): Extension, Extension(principal): Extension, payload: Result< Json, JsonRejection, >, ) -> Result, Response> { require_scope_response(&request_context, &principal, SCOPE_EDITOR_IMAGE_GENERATE)?; generate_editor_character_animation_for_owner( state, request_context, principal.owner_user_id().to_string(), payload, ) .await } pub async fn generate_external_editor_video( State(state): State, Extension(request_context): Extension, Extension(principal): Extension, payload: Result, JsonRejection>, ) -> Result, Response> { require_scope_response(&request_context, &principal, SCOPE_EDITOR_IMAGE_GENERATE)?; generate_editor_video_for_owner( state, request_context, principal.owner_user_id().to_string(), payload, ) .await } pub async fn generate_external_editor_sound_effect( State(state): State, Extension(request_context): Extension, Extension(principal): Extension, payload: Result< Json, JsonRejection, >, ) -> Result, Response> { require_scope_response(&request_context, &principal, SCOPE_EDITOR_IMAGE_GENERATE)?; generate_editor_sound_effect_for_owner( state, request_context, principal.owner_user_id().to_string(), payload, ) .await } pub async fn generate_external_editor_background_music( State(state): State, Extension(request_context): Extension, Extension(principal): Extension, payload: Result< Json, JsonRejection, >, ) -> Result, Response> { require_scope_response(&request_context, &principal, SCOPE_EDITOR_IMAGE_GENERATE)?; generate_editor_background_music_for_owner( state, request_context, principal.owner_user_id().to_string(), payload, ) .await } fn editor_generation_caller( principal: &ExternalApiPrincipal, project_id: Option, ) -> EditorGenerationCaller { EditorGenerationCaller { owner_user_id: principal.owner_user_id().to_string(), audit_subject_user_id: Some(principal.owner_user_id().to_string()), audit_project_id: normalize_optional_string(project_id), } } fn require_scope(principal: &ExternalApiPrincipal, scope: &str) -> Result<(), AppError> { if principal.has_scope(scope) { return Ok(()); } Err( AppError::from_status(StatusCode::FORBIDDEN).with_details(json!({ "provider": EXTERNAL_EDITOR_PROVIDER, "keyId": principal.key_id(), "scope": scope, "message": "API Key 缺少所需权限", })), ) } fn require_scope_response( request_context: &RequestContext, principal: &ExternalApiPrincipal, scope: &str, ) -> Result<(), Response> { require_scope(principal, scope) .map_err(|error| error.into_response_with_context(Some(request_context))) } fn normalize_project_title(title: Option) -> String { title .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) .unwrap_or_else(|| EDITOR_PROJECT_DEFAULT_TITLE.to_string()) } #[cfg(test)] mod tests { use super::*; #[test] fn exported_openapi_json_contains_external_editor_routes_and_security() { let parsed: Value = serde_json::from_str(OPENAPI_JSON).expect("openapi json should parse"); assert_eq!(parsed["openapi"], "3.1.0"); assert!( parsed["paths"] .get("/api/external/v1/editor/projects") .is_some() ); assert!( parsed["paths"] .get("/api/external/v1/assets/direct-upload-tickets") .is_some() ); assert!( parsed["paths"] .get("/api/external/v1/assets/objects/confirm") .is_some() ); assert!( parsed["paths"] .get("/api/external/v1/assets/read-url") .is_some() ); assert!( parsed["paths"]["/api/external/v1/editor/projects"] .get("get") .is_some() ); assert!( parsed["paths"]["/api/external/v1/editor/projects"] .get("post") .is_some() ); assert!( parsed["paths"] .get("/api/external/v1/editor/projects/recent") .is_some() ); assert!( parsed["paths"]["/api/external/v1/editor/projects/{projectId}"] .get("delete") .is_some() ); assert!( parsed["paths"] .get("/api/external/v1/editor/projects/{projectId}/metadata") .is_some() ); assert!( parsed["paths"] .get("/api/external/v1/editor/images/generations") .is_some() ); assert!( parsed["components"]["schemas"]["EditorImageGenerationRequest"]["required"] .as_array() .is_some_and(|required| !required.contains(&json!("priceMudPoints"))) ); assert!( parsed["components"]["schemas"]["EditorImageGenerationRequest"]["properties"] .get("priceMudPoints") .is_none() ); assert!( parsed["paths"] .get("/api/external/v1/editor/images/edits") .is_some() ); assert!( parsed["components"]["schemas"]["EditorImageEditRequest"]["required"] .as_array() .is_some_and(|required| !required.contains(&json!("priceMudPoints"))) ); assert!( parsed["components"]["schemas"]["EditorImageEditRequest"]["properties"] .get("priceMudPoints") .is_none() ); assert!( parsed["components"]["schemas"]["EditorImageEditRequest"]["properties"] .get("targetLayerId") .is_some() ); assert!( parsed["paths"] .get("/api/external/v1/editor/icon-spritesheets/generations") .is_some() ); assert!( parsed["paths"] .get("/api/external/v1/editor/ui-designs/assets/extractions") .is_some() ); let ui_extraction_schema = &parsed["components"]["schemas"]["EditorUiDesignAssetExtractionRequest"]; assert_eq!( ui_extraction_schema["properties"]["aspectRatio"]["const"], "1:1" ); assert_eq!( ui_extraction_schema["properties"]["imageSize"]["enum"], json!(["1K", "2K"]) ); assert_eq!( ui_extraction_schema["properties"]["model"]["default"], "gemini-3.1-flash-image-preview" ); assert!( ui_extraction_schema["properties"] .get("priceMudPoints") .is_none() ); assert!( parsed["paths"] .get("/api/external/v1/editor/videos/generations") .is_some() ); assert!( parsed["paths"] .get("/api/external/v1/editor/audios/sound-effects/generations") .is_some() ); assert!( parsed["paths"] .get("/api/external/v1/editor/audios/background-music/generations") .is_some() ); assert!( parsed["paths"] .get("/api/external/v1/editor/assets/library") .is_some() ); assert!( parsed["paths"] .get("/api/external/v1/editor/projects/{projectId}/resources") .is_some() ); assert!( parsed["paths"] .get("/api/external/v1/editor/assets/folders") .is_some() ); assert!( parsed["paths"] .get("/api/external/v1/editor/assets/{assetId}") .is_some() ); assert!(parsed["paths"].get("/api/profile/api-keys").is_none()); assert!( parsed["components"]["securitySchemes"] .get("UserAccessToken") .is_none() ); assert_eq!( parsed["components"]["securitySchemes"]["ExternalApiKey"]["scheme"], "bearer" ); } }