@@ -395,6 +395,195 @@ it.effect("supplies documented scopes when Picker Discovery omits auth metadata"
395395 } ) ,
396396) ;
397397
398+ it . effect (
399+ "generates a separate media-upload operation for Google Discovery methods with supportsMediaUpload" ,
400+ ( ) =>
401+ Effect . gen ( function * ( ) {
402+ const result = yield * convertGoogleDiscoveryToOpenApi ( {
403+ discoveryUrl : "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest" ,
404+ // @effect -diagnostics-next-line preferSchemaOverJson:off
405+ documentText : JSON . stringify ( {
406+ name : "drive" ,
407+ version : "v3" ,
408+ title : "Drive API" ,
409+ rootUrl : "https://www.googleapis.com/" ,
410+ servicePath : "drive/v3/" ,
411+ resources : {
412+ files : {
413+ methods : {
414+ create : {
415+ id : "drive.files.create" ,
416+ httpMethod : "POST" ,
417+ path : "files" ,
418+ request : { $ref : "File" } ,
419+ response : { $ref : "File" } ,
420+ supportsMediaUpload : true ,
421+ mediaUpload : {
422+ accept : [ "*/*" ] ,
423+ maxSize : "5497558138880" ,
424+ protocols : {
425+ simple : {
426+ multipart : true ,
427+ path : "/upload/drive/v3/files" ,
428+ } ,
429+ resumable : {
430+ multipart : true ,
431+ path : "/resumable/upload/drive/v3/files" ,
432+ } ,
433+ } ,
434+ } ,
435+ scopes : [ "https://www.googleapis.com/auth/drive.file" ] ,
436+ parameters : {
437+ enforceSingleParent : {
438+ location : "query" ,
439+ type : "boolean" ,
440+ } ,
441+ } ,
442+ } ,
443+ update : {
444+ id : "drive.files.update" ,
445+ httpMethod : "PATCH" ,
446+ path : "files/{fileId}" ,
447+ request : { $ref : "File" } ,
448+ response : { $ref : "File" } ,
449+ supportsMediaUpload : true ,
450+ mediaUpload : {
451+ accept : [ "*/*" ] ,
452+ protocols : {
453+ simple : {
454+ multipart : true ,
455+ path : "/upload/drive/v3/files/{fileId}" ,
456+ } ,
457+ } ,
458+ } ,
459+ scopes : [ "https://www.googleapis.com/auth/drive.file" ] ,
460+ parameters : {
461+ fileId : { location : "path" , required : true , type : "string" } ,
462+ } ,
463+ } ,
464+ export : {
465+ id : "drive.files.export" ,
466+ httpMethod : "GET" ,
467+ path : "files/{fileId}/export" ,
468+ supportsMediaDownload : true ,
469+ useMediaDownloadService : true ,
470+ parameters : {
471+ fileId : { location : "path" , required : true , type : "string" } ,
472+ mimeType : { location : "query" , required : true , type : "string" } ,
473+ } ,
474+ } ,
475+ } ,
476+ } ,
477+ } ,
478+ schemas : {
479+ File : {
480+ id : "File" ,
481+ type : "object" ,
482+ properties : {
483+ name : { type : "string" } ,
484+ } ,
485+ } ,
486+ } ,
487+ } ) ,
488+ } ) ;
489+
490+ const spec = decodeConvertedSpec ( result . specText ) ;
491+ // The metadata-only operation should keep the original JSON behavior.
492+ const createMetadata = spec . paths [ "/files" ] ?. post ;
493+ expect ( createMetadata ) . toMatchObject ( {
494+ operationId : "files.create" ,
495+ "x-executor-toolPath" : "files.create" ,
496+ requestBody : {
497+ content : {
498+ "application/json" : {
499+ schema : { $ref : "#/components/schemas/File" } ,
500+ } ,
501+ } ,
502+ } ,
503+ } ) ;
504+
505+ // A separate media-upload operation should be emitted with the simple upload path.
506+ const createMedia = spec . paths [ "/upload/drive/v3/files" ] ?. post ;
507+ expect ( createMedia ) . toMatchObject ( {
508+ operationId : "files.createMedia" ,
509+ "x-executor-toolPath" : "files.createMedia" ,
510+ "x-executor-pathTemplate" : "/upload/drive/v3/files" ,
511+ } ) ;
512+ expect ( createMedia ?. parameters ) . toContainEqual (
513+ expect . objectContaining ( {
514+ name : "uploadType" ,
515+ in : "query" ,
516+ required : true ,
517+ schema : {
518+ type : "string" ,
519+ description : "The upload type for the media upload." ,
520+ enum : [ "media" ] ,
521+ default : "media" ,
522+ } ,
523+ } ) ,
524+ ) ;
525+ expect ( createMedia ?. requestBody ) . toMatchObject ( {
526+ content : {
527+ "application/octet-stream" : {
528+ schema : { type : "string" , format : "binary" } ,
529+ } ,
530+ } ,
531+ } ) ;
532+
533+ // The updateMedia operation should preserve path parameters from the original method.
534+ const updateMedia = spec . paths [ "/upload/drive/v3/files/{fileId}" ] ?. patch ;
535+ expect ( updateMedia ) . toMatchObject ( {
536+ operationId : "files.updateMedia" ,
537+ "x-executor-toolPath" : "files.updateMedia" ,
538+ "x-executor-pathTemplate" : "/upload/drive/v3/files/{fileId}" ,
539+ } ) ;
540+ expect ( updateMedia ?. parameters ) . toContainEqual (
541+ expect . objectContaining ( {
542+ name : "fileId" ,
543+ in : "path" ,
544+ required : true ,
545+ } ) ,
546+ ) ;
547+ expect ( updateMedia ?. parameters ) . toContainEqual (
548+ expect . objectContaining ( {
549+ name : "uploadType" ,
550+ in : "query" ,
551+ required : true ,
552+ schema : {
553+ type : "string" ,
554+ description : "The upload type for the media upload." ,
555+ enum : [ "media" ] ,
556+ default : "media" ,
557+ } ,
558+ } ) ,
559+ ) ;
560+
561+ // Extraction should produce a usable input schema that includes bodyBase64.
562+ const parsed = yield * parse ( result . specText ) ;
563+ const extracted = yield * extract ( parsed ) ;
564+ const createMediaOp = extracted . operations . find (
565+ ( candidate ) => candidate . operationId === "files.createMedia" ,
566+ ) ;
567+ expect ( createMediaOp ?. operationId ) . toBe ( "files.createMedia" ) ;
568+ const inputSchema = createMediaOp
569+ ? ( Option . getOrUndefined ( createMediaOp . inputSchema ) as
570+ | {
571+ properties ?: Record < string , unknown > ;
572+ required ?: string [ ] ;
573+ }
574+ | undefined )
575+ : undefined ;
576+ expect ( inputSchema ) . toBeDefined ( ) ;
577+ expect ( inputSchema ?. properties ?. bodyBase64 ) . toMatchObject ( {
578+ type : "string" ,
579+ contentEncoding : "base64" ,
580+ contentMediaType : "application/octet-stream" ,
581+ } ) ;
582+ expect ( inputSchema ?. required ) . toContain ( "bodyBase64" ) ;
583+ expect ( inputSchema ?. properties ?. body ) . toBeUndefined ( ) ;
584+ } ) ,
585+ ) ;
586+
398587it . effect ( "bundles Google Discovery documents into one Google OpenAPI source" , ( ) =>
399588 Effect . gen ( function * ( ) {
400589 const result = yield * convertGoogleDiscoveryBundleToOpenApi ( {
@@ -631,6 +820,91 @@ const decodeConvertedSpecSecurity = Schema.decodeUnknownSync(
631820 Schema . fromJsonString ( ConvertedSpecSecurity ) ,
632821) ;
633822
823+ it . effect ( "generates media-upload operations for bundled Google Discovery documents" , ( ) =>
824+ Effect . gen ( function * ( ) {
825+ const result = yield * convertGoogleDiscoveryBundleToOpenApi ( {
826+ documents : [
827+ {
828+ discoveryUrl : "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest" ,
829+ // @effect -diagnostics-next-line preferSchemaOverJson:off
830+ documentText : JSON . stringify ( {
831+ name : "drive" ,
832+ version : "v3" ,
833+ title : "Drive API" ,
834+ rootUrl : "https://www.googleapis.com/" ,
835+ servicePath : "drive/v3/" ,
836+ auth : {
837+ oauth2 : {
838+ scopes : {
839+ "https://www.googleapis.com/auth/drive.file" : {
840+ description : "Drive file access" ,
841+ } ,
842+ } ,
843+ } ,
844+ } ,
845+ resources : {
846+ files : {
847+ methods : {
848+ create : {
849+ id : "drive.files.create" ,
850+ httpMethod : "POST" ,
851+ path : "files" ,
852+ request : { $ref : "File" } ,
853+ response : { $ref : "File" } ,
854+ supportsMediaUpload : true ,
855+ mediaUpload : {
856+ accept : [ "*/*" ] ,
857+ protocols : {
858+ simple : {
859+ multipart : true ,
860+ path : "/upload/drive/v3/files" ,
861+ } ,
862+ } ,
863+ } ,
864+ scopes : [ "https://www.googleapis.com/auth/drive.file" ] ,
865+ parameters : { } ,
866+ } ,
867+ } ,
868+ } ,
869+ } ,
870+ schemas : {
871+ File : {
872+ id : "File" ,
873+ type : "object" ,
874+ properties : {
875+ name : { type : "string" } ,
876+ } ,
877+ } ,
878+ } ,
879+ } ) ,
880+ } ,
881+ ] ,
882+ } ) ;
883+
884+ const spec = decodeConvertedSpec ( result . specText ) ;
885+ const createMedia = spec . paths [ "/upload/drive/v3/files" ] ?. post ;
886+ expect ( createMedia ) . toMatchObject ( {
887+ operationId : "drive.files.createMedia" ,
888+ "x-executor-toolPath" : "drive.files.createMedia" ,
889+ "x-executor-pathTemplate" : "/upload/drive/v3/files" ,
890+ } ) ;
891+ expect ( createMedia ?. parameters ) . toContainEqual (
892+ expect . objectContaining ( {
893+ name : "uploadType" ,
894+ in : "query" ,
895+ required : true ,
896+ } ) ,
897+ ) ;
898+ expect ( createMedia ?. requestBody ) . toMatchObject ( {
899+ content : {
900+ "application/octet-stream" : {
901+ schema : { type : "string" , format : "binary" } ,
902+ } ,
903+ } ,
904+ } ) ;
905+ } ) ,
906+ ) ;
907+
634908it . effect ( "compacts and filters the merged bundle scope set into a clean consent set" , ( ) =>
635909 Effect . gen ( function * ( ) {
636910 const result = yield * convertGoogleDiscoveryBundleToOpenApi ( {
0 commit comments