radiant-docs 0.1.64 → 0.1.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/package.json +4 -3
  2. package/template/package-lock.json +1991 -4096
  3. package/template/package.json +7 -19
  4. package/template/src/components/OpenApiPage.astro +107 -18
  5. package/template/src/components/Sidebar.astro +1 -0
  6. package/template/src/components/endpoint/PlaygroundBar.astro +1 -1
  7. package/template/src/components/endpoint/PlaygroundField.astro +433 -71
  8. package/template/src/components/endpoint/PlaygroundForm.astro +470 -87
  9. package/template/src/components/endpoint/RequestSnippets.astro +6 -1
  10. package/template/src/components/endpoint/ResponseDisplay.astro +109 -2
  11. package/template/src/components/endpoint/ResponseFieldTree.astro +1 -1
  12. package/template/src/components/endpoint/ResponseFields.astro +97 -28
  13. package/template/src/components/endpoint/ResponseSnippets.astro +19 -7
  14. package/template/src/layouts/Layout.astro +94 -0
  15. package/template/src/lib/openapi/operation-doc.ts +308 -70
  16. package/template/src/lib/openapi/response-content.ts +133 -0
  17. package/template/src/lib/openapi/schema-variant-labels.ts +213 -0
  18. package/template/.vscode/extensions.json +0 -4
  19. package/template/.vscode/launch.json +0 -11
  20. package/template/scripts/generate-og-images.mjs +0 -667
  21. package/template/scripts/generate-og-metadata.mjs +0 -206
  22. package/template/scripts/generate-proxy-allowed-origins.mjs +0 -48
  23. package/template/scripts/generate-robots-txt.mjs +0 -47
  24. package/template/scripts/publish-shiki-platform-assets.mjs +0 -1177
  25. package/template/scripts/remove-assistant-for-non-pro.mjs +0 -28
  26. package/template/scripts/stamp-image-versions.mjs +0 -355
  27. package/template/scripts/stamp-og-image-versions.mjs +0 -199
  28. package/template/scripts/stamp-pagefind-runtime-version.mjs +0 -140
@@ -2,6 +2,7 @@ import type { HttpMethods } from "oas/types";
2
2
  import type { OpenApiRoute } from "../routes";
3
3
  import { getOasInstance } from "../oas";
4
4
  import { loadOpenApiSpec } from "../validation";
5
+ import { getSchemaVariantLabel } from "./schema-variant-labels";
5
6
 
6
7
  export const OPENAPI_REQUEST_SECTION_KEYS = [
7
8
  "header",
@@ -30,6 +31,11 @@ export interface OpenApiFieldVariant {
30
31
  fields: OpenApiField[];
31
32
  }
32
33
 
34
+ export interface OpenApiFieldValueVariant {
35
+ label: string;
36
+ field: OpenApiField;
37
+ }
38
+
33
39
  export interface OpenApiField {
34
40
  name: string;
35
41
  required: boolean;
@@ -50,8 +56,13 @@ export interface OpenApiField {
50
56
  nested?: OpenApiField[];
51
57
  variants?: OpenApiFieldVariant[];
52
58
  variantType?: "oneOf" | "anyOf";
59
+ valueVariants?: OpenApiFieldValueVariant[];
60
+ valueVariantType?: "oneOf" | "anyOf";
53
61
  isAdditionalProperty?: boolean;
54
62
  mapKnownKeys?: string[];
63
+ renderAsJson?: boolean;
64
+ isFile?: boolean;
65
+ valuePrefix?: string;
55
66
  }
56
67
 
57
68
  export type OpenApiRequestFields = Record<
@@ -84,6 +95,7 @@ export interface OpenApiOperationDoc {
84
95
  >;
85
96
  bodyDescription: string;
86
97
  bodyDefaultKind?: OpenApiBodyDefaultKind;
98
+ requestBodyContentType?: string;
87
99
  }
88
100
 
89
101
  function createEmptyRequestFields(): OpenApiRequestFields {
@@ -109,6 +121,44 @@ function getSchemaVariantType(schema: any): "oneOf" | "anyOf" | undefined {
109
121
  return undefined;
110
122
  }
111
123
 
124
+ function isNullSchema(schema: any): boolean {
125
+ if (!schema || typeof schema !== "object") return false;
126
+ if (schema.type === "null") return true;
127
+ return Array.isArray(schema.type) && schema.type.includes("null");
128
+ }
129
+
130
+ function unwrapNullableSchema(schema: any, seen = new Set<any>()): any {
131
+ if (!schema || typeof schema !== "object") return schema;
132
+ if (seen.has(schema)) return schema;
133
+
134
+ const nextSeen = new Set(seen);
135
+ nextSeen.add(schema);
136
+
137
+ if (Array.isArray(schema.type) && schema.type.includes("null")) {
138
+ const nonNullTypes = schema.type.filter((type: string) => type !== "null");
139
+ if (nonNullTypes.length === 1) {
140
+ return unwrapNullableSchema(
141
+ {
142
+ ...schema,
143
+ type: nonNullTypes[0],
144
+ },
145
+ nextSeen,
146
+ );
147
+ }
148
+ }
149
+
150
+ const variantType = getSchemaVariantType(schema);
151
+ if (!variantType) return schema;
152
+
153
+ const variants = getSchemaVariants(schema);
154
+ const nonNullVariants = variants.filter((variant) => !isNullSchema(variant));
155
+ if (nonNullVariants.length === 1 && nonNullVariants.length < variants.length) {
156
+ return unwrapNullableSchema(nonNullVariants[0], nextSeen);
157
+ }
158
+
159
+ return schema;
160
+ }
161
+
112
162
  function getAdditionalPropertiesSchema(schema: any): any {
113
163
  if (!schema || typeof schema !== "object") return undefined;
114
164
 
@@ -125,6 +175,60 @@ function getAdditionalPropertiesSchema(schema: any): any {
125
175
  return additionalProperties;
126
176
  }
127
177
 
178
+ function isEmptySchema(schema: any): boolean {
179
+ return (
180
+ schema &&
181
+ typeof schema === "object" &&
182
+ !Array.isArray(schema) &&
183
+ Object.keys(schema).length === 0
184
+ );
185
+ }
186
+
187
+ function getSchemaDefault(schema: any, seen = new Set<any>()): {
188
+ hasDefault: boolean;
189
+ defaultValue?: unknown;
190
+ } {
191
+ if (!schema || typeof schema !== "object") return { hasDefault: false };
192
+
193
+ if (Object.prototype.hasOwnProperty.call(schema, "default")) {
194
+ return { hasDefault: true, defaultValue: schema.default };
195
+ }
196
+
197
+ if (seen.has(schema)) return { hasDefault: false };
198
+
199
+ const nextSeen = new Set(seen);
200
+ nextSeen.add(schema);
201
+
202
+ const variantType = getSchemaVariantType(schema);
203
+ if (!variantType) return { hasDefault: false };
204
+
205
+ const variants = getSchemaVariants(schema);
206
+ const nonNullVariants = variants.filter((variant) => !isNullSchema(variant));
207
+ if (nonNullVariants.length === 1 && nonNullVariants.length < variants.length) {
208
+ return getSchemaDefault(nonNullVariants[0], nextSeen);
209
+ }
210
+
211
+ return { hasDefault: false };
212
+ }
213
+
214
+ function shouldRenderSchemaAsJson(schema: any): boolean {
215
+ const renderSchema = unwrapNullableSchema(schema);
216
+ if (!renderSchema || typeof renderSchema !== "object") return false;
217
+ if (getSchemaStructuralType(renderSchema) !== "object") return false;
218
+ if (getSchemaVariantType(renderSchema)) return false;
219
+
220
+ const shape = getTopLevelObjectShape(renderSchema);
221
+ if (Object.keys(shape.properties).length > 0) return false;
222
+
223
+ const additionalProperties = getAdditionalPropertiesSchema(renderSchema);
224
+ return additionalProperties === true || isEmptySchema(additionalProperties);
225
+ }
226
+
227
+ function isFileUploadSchema(schema: any): boolean {
228
+ const renderSchema = unwrapNullableSchema(schema);
229
+ return renderSchema?.type === "string" && renderSchema?.format === "binary";
230
+ }
231
+
128
232
  function getSchemaEnumValues(schema: any): (string | number)[] | undefined {
129
233
  if (!schema) return undefined;
130
234
  if (Array.isArray(schema.enum) && schema.enum.length > 0) {
@@ -501,19 +605,33 @@ function getSchemaStructuralType(
501
605
  ): OpenApiBodyDefaultKind | undefined {
502
606
  if (!schema || typeof schema !== "object") return undefined;
503
607
 
608
+ const renderSchema = unwrapNullableSchema(schema);
609
+ if (renderSchema !== schema) return getSchemaStructuralType(renderSchema);
610
+
504
611
  if (schema.type === "object") return "object";
505
612
  if (schema.type === "array") return "array";
506
613
 
507
614
  const variants = getSchemaVariants(schema);
508
615
  if (variants.length > 0) {
616
+ const nonNullVariants = variants.filter((variant) => !isNullSchema(variant));
617
+ const structuralVariantTypes = nonNullVariants.map((variant) =>
618
+ getSchemaStructuralType(variant),
619
+ );
620
+ if (
621
+ structuralVariantTypes.some(
622
+ (variantType) =>
623
+ variantType !== "object" && variantType !== "array",
624
+ )
625
+ ) {
626
+ return undefined;
627
+ }
628
+
509
629
  const variantTypes = Array.from(
510
630
  new Set(
511
- variants
512
- .map((variant) => getSchemaStructuralType(variant))
513
- .filter(
514
- (variantType): variantType is OpenApiBodyDefaultKind =>
515
- variantType === "object" || variantType === "array",
516
- ),
631
+ structuralVariantTypes.filter(
632
+ (variantType): variantType is OpenApiBodyDefaultKind =>
633
+ variantType === "object" || variantType === "array",
634
+ ),
517
635
  ),
518
636
  );
519
637
  if (variantTypes.length === 1) return variantTypes[0];
@@ -609,6 +727,139 @@ function getTopLevelObjectShape(schema: any): {
609
727
  return { properties, required, hasObjectShape };
610
728
  }
611
729
 
730
+ function getSchemaVariantObjectProperties(schema: any): Record<string, any> {
731
+ return getTopLevelObjectShape(schema).properties;
732
+ }
733
+
734
+ function getSchemaDescription(schema: any): string {
735
+ if (!schema || typeof schema !== "object") return "";
736
+ if (typeof schema.description === "string") return schema.description;
737
+
738
+ const renderSchema = unwrapNullableSchema(schema);
739
+ if (
740
+ renderSchema &&
741
+ renderSchema !== schema &&
742
+ typeof renderSchema.description === "string"
743
+ ) {
744
+ return renderSchema.description;
745
+ }
746
+
747
+ return "";
748
+ }
749
+
750
+ function getValueVariantData(
751
+ schema: any,
752
+ ): { variantType: "oneOf" | "anyOf"; schemas: any[] } | undefined {
753
+ const renderSchema = unwrapNullableSchema(schema);
754
+ const variantType = getSchemaVariantType(renderSchema);
755
+ if (!variantType) return undefined;
756
+
757
+ const variantSchemas = getSchemaVariants(renderSchema)
758
+ .filter((variant) => !isNullSchema(variant))
759
+ .map((variant) => unwrapNullableSchema(variant));
760
+
761
+ if (variantSchemas.length < 2) return undefined;
762
+
763
+ const variantShapeTypes = variantSchemas.map((variantSchema) => {
764
+ const structuralType = getSchemaStructuralType(variantSchema);
765
+ if (structuralType === "array" || structuralType === "object") {
766
+ return structuralType;
767
+ }
768
+ if (getTopLevelObjectShape(variantSchema).hasObjectShape) return "object";
769
+ return "scalar";
770
+ });
771
+
772
+ const hasArrayShape = variantShapeTypes.includes("array");
773
+ const hasObjectShape = variantShapeTypes.includes("object");
774
+ const hasScalarShape = variantShapeTypes.includes("scalar");
775
+ const hasValueShapeChoice =
776
+ hasArrayShape || (hasObjectShape && hasScalarShape);
777
+
778
+ if (!hasValueShapeChoice) return undefined;
779
+ return { variantType, schemas: variantSchemas };
780
+ }
781
+
782
+ function createOpenApiFieldFromSchema(args: {
783
+ name: string;
784
+ required: boolean;
785
+ schema: any;
786
+ seen: Set<any>;
787
+ description?: string;
788
+ style?: string;
789
+ explode?: boolean;
790
+ isAdditionalProperty?: boolean;
791
+ mapKnownKeys?: string[];
792
+ includeValueVariants?: boolean;
793
+ }): OpenApiField {
794
+ const {
795
+ name,
796
+ required,
797
+ schema,
798
+ seen,
799
+ description,
800
+ style,
801
+ explode,
802
+ isAdditionalProperty,
803
+ mapKnownKeys,
804
+ includeValueVariants = true,
805
+ } = args;
806
+ const renderSchema = unwrapNullableSchema(schema);
807
+ const valueVariantData = includeValueVariants
808
+ ? getValueVariantData(renderSchema)
809
+ : undefined;
810
+ const nestedSchema = valueVariantData ? undefined : renderSchema;
811
+ const nestedContent = nestedSchema
812
+ ? extractRequestSchemaContent(nestedSchema, seen)
813
+ : { fields: [] as OpenApiField[] };
814
+ const schemaDefault = getSchemaDefault(schema);
815
+
816
+ const field: OpenApiField = {
817
+ name,
818
+ required,
819
+ type: getSchemaTypeLabel(schema),
820
+ description: description || getSchemaDescription(schema),
821
+ enum: getSchemaEnumValues(schema),
822
+ ...getSchemaStringLengthConstraints(schema, seen),
823
+ ...getSchemaNumericConstraints(schema, seen),
824
+ hasDefault: schemaDefault.hasDefault,
825
+ defaultValue: schemaDefault.defaultValue,
826
+ isArray: getSchemaStructuralType(renderSchema) === "array",
827
+ style,
828
+ explode,
829
+ nested: nestedContent.fields.length > 0 ? nestedContent.fields : undefined,
830
+ variants: nestedContent.variants,
831
+ variantType: nestedContent.variantType,
832
+ isAdditionalProperty,
833
+ mapKnownKeys,
834
+ renderAsJson: shouldRenderSchemaAsJson(renderSchema),
835
+ isFile: isFileUploadSchema(renderSchema),
836
+ };
837
+
838
+ if (valueVariantData) {
839
+ field.valueVariants = valueVariantData.schemas.map(
840
+ (variantSchema, index) => ({
841
+ label: getSchemaVariantLabel(variantSchema, index, {
842
+ parentSchema: renderSchema,
843
+ siblings: valueVariantData.schemas,
844
+ getObjectProperties: getSchemaVariantObjectProperties,
845
+ getTypeLabel: getSchemaTypeLabel,
846
+ }),
847
+ field: createOpenApiFieldFromSchema({
848
+ name,
849
+ required,
850
+ schema: variantSchema,
851
+ seen,
852
+ description: getSchemaDescription(variantSchema),
853
+ includeValueVariants: false,
854
+ }),
855
+ }),
856
+ );
857
+ field.valueVariantType = valueVariantData.variantType;
858
+ }
859
+
860
+ return field;
861
+ }
862
+
612
863
  function extractRequestSchemaContent(
613
864
  schema: any,
614
865
  seen = new Set<any>(),
@@ -642,29 +893,12 @@ function extractRequestSchemaContent(
642
893
  return 0;
643
894
  })
644
895
  .map(([name, propertySchema]: [string, any]) => {
645
- const nestedContent = extractRequestSchemaContent(
646
- propertySchema,
647
- nextSeen,
648
- );
649
- return {
896
+ return createOpenApiFieldFromSchema({
650
897
  name,
651
898
  required: shape.required.has(name),
652
- type: getSchemaTypeLabel(propertySchema),
653
- description: propertySchema?.description || "",
654
- enum: getSchemaEnumValues(propertySchema),
655
- ...getSchemaStringLengthConstraints(propertySchema, nextSeen),
656
- ...getSchemaNumericConstraints(propertySchema, nextSeen),
657
- hasDefault: Object.prototype.hasOwnProperty.call(
658
- propertySchema || {},
659
- "default",
660
- ),
661
- defaultValue: propertySchema?.default,
662
- isArray: propertySchema?.type === "array",
663
- nested:
664
- nestedContent.fields.length > 0 ? nestedContent.fields : undefined,
665
- variants: nestedContent.variants,
666
- variantType: nestedContent.variantType,
667
- };
899
+ schema: propertySchema,
900
+ seen: nextSeen,
901
+ });
668
902
  });
669
903
 
670
904
  const additionalProperties = getAdditionalPropertiesSchema(baseSchema);
@@ -679,35 +913,16 @@ function extractRequestSchemaContent(
679
913
  additionalProperties && typeof additionalProperties === "object"
680
914
  ? additionalProperties
681
915
  : null;
682
- const additionalContent = additionalSchema
683
- ? extractRequestSchemaContent(additionalSchema, nextSeen)
684
- : { fields: [] as OpenApiField[] };
685
916
 
686
- fields.push({
917
+ fields.push(createOpenApiFieldFromSchema({
687
918
  name: "[key: string]",
688
919
  required: false,
689
- type: additionalSchema ? getSchemaTypeLabel(additionalSchema) : "any",
920
+ schema: additionalSchema,
921
+ seen: nextSeen,
690
922
  description: additionalSchema?.description || "",
691
- enum: additionalSchema
692
- ? getSchemaEnumValues(additionalSchema)
693
- : undefined,
694
- ...getSchemaStringLengthConstraints(additionalSchema, nextSeen),
695
- ...getSchemaNumericConstraints(additionalSchema, nextSeen),
696
- hasDefault: Object.prototype.hasOwnProperty.call(
697
- additionalSchema || {},
698
- "default",
699
- ),
700
- defaultValue: additionalSchema?.default,
701
- isArray: additionalSchema?.type === "array",
702
- nested:
703
- additionalContent.fields.length > 0
704
- ? additionalContent.fields
705
- : undefined,
706
- variants: additionalContent.variants,
707
- variantType: additionalContent.variantType,
708
923
  isAdditionalProperty: true,
709
924
  mapKnownKeys: Object.keys(shape.properties),
710
- });
925
+ }));
711
926
  }
712
927
 
713
928
  const variantType = getSchemaVariantType(schema);
@@ -720,10 +935,28 @@ function extractRequestSchemaContent(
720
935
  const variants =
721
936
  variantType && variantSchemas.length > 0
722
937
  ? variantSchemas
723
- .map((variantSchema: any, index: number) => ({
724
- label: `Variant ${index + 1}`,
725
- fields: extractRequestSchemaContent(variantSchema, nextSeen).fields,
726
- }))
938
+ .flatMap((variantSchema: any, index: number) => {
939
+ const variantContent = extractRequestSchemaContent(
940
+ variantSchema,
941
+ nextSeen,
942
+ );
943
+
944
+ if (variantContent.fields.length > 0) {
945
+ return [
946
+ {
947
+ label: getSchemaVariantLabel(variantSchema, index, {
948
+ parentSchema: schema,
949
+ siblings: variantSchemas,
950
+ getObjectProperties: getSchemaVariantObjectProperties,
951
+ getTypeLabel: getSchemaTypeLabel,
952
+ }),
953
+ fields: variantContent.fields,
954
+ },
955
+ ];
956
+ }
957
+
958
+ return variantContent.variants || [];
959
+ })
727
960
  .filter((variant: OpenApiFieldVariant) => variant.fields.length > 0)
728
961
  : [];
729
962
 
@@ -773,6 +1006,7 @@ function addSecurityFields(args: {
773
1006
  fieldData.name = "Authorization";
774
1007
 
775
1008
  if (scheme.scheme === "bearer") {
1009
+ fieldData.valuePrefix = "Bearer";
776
1010
  fieldData.description =
777
1011
  fieldData.description || "Bearer token authentication.";
778
1012
  } else if (scheme.scheme === "basic") {
@@ -784,6 +1018,7 @@ function addSecurityFields(args: {
784
1018
  args.requestFields.header.push(fieldData);
785
1019
  } else if (scheme.type === "oauth2" || scheme.type === "openIdConnect") {
786
1020
  fieldData.name = "Authorization";
1021
+ fieldData.valuePrefix = "Bearer";
787
1022
  fieldData.description = fieldData.description || "OAuth2 Bearer Token.";
788
1023
 
789
1024
  args.requestFields.header.push(fieldData);
@@ -801,6 +1036,7 @@ function buildOpenApiRequestModel(args: {
801
1036
  >;
802
1037
  bodyDescription: string;
803
1038
  bodyDefaultKind?: OpenApiBodyDefaultKind;
1039
+ requestBodyContentType?: string;
804
1040
  } {
805
1041
  const requestFields = createEmptyRequestFields();
806
1042
  const requestSectionVariants: Partial<
@@ -808,6 +1044,7 @@ function buildOpenApiRequestModel(args: {
808
1044
  > = {};
809
1045
  let bodyDescription = "";
810
1046
  let bodyDefaultKind: OpenApiBodyDefaultKind | undefined = undefined;
1047
+ let requestBodyContentType: string | undefined = undefined;
811
1048
 
812
1049
  addSecurityFields({
813
1050
  operation: args.operation,
@@ -818,26 +1055,15 @@ function buildOpenApiRequestModel(args: {
818
1055
  const parameters = args.operation.getParameters();
819
1056
  parameters.forEach((param: any) => {
820
1057
  const schema = param.schema as any;
821
- const isArray = schema?.type === "array";
822
- const schemaContent = extractRequestSchemaContent(schema);
823
-
824
- const field: OpenApiField = {
1058
+ const field = createOpenApiFieldFromSchema({
825
1059
  name: param.name,
826
1060
  required: param.required || false,
827
- type: getSchemaTypeLabel(schema),
1061
+ schema,
1062
+ seen: new Set<any>(),
828
1063
  description: param.description || "",
829
- enum: getSchemaEnumValues(schema),
830
- ...getSchemaStringLengthConstraints(schema),
831
- ...getSchemaNumericConstraints(schema),
832
- hasDefault: Object.prototype.hasOwnProperty.call(schema || {}, "default"),
833
- defaultValue: schema?.default,
834
- isArray,
835
1064
  style: param.style,
836
1065
  explode: param.explode,
837
- nested: schemaContent.fields.length > 0 ? schemaContent.fields : undefined,
838
- variants: schemaContent.variants,
839
- variantType: schemaContent.variantType,
840
- };
1066
+ });
841
1067
 
842
1068
  if (param.in === "path") requestFields.path.push(field);
843
1069
  if (param.in === "query") requestFields.query.push(field);
@@ -846,8 +1072,19 @@ function buildOpenApiRequestModel(args: {
846
1072
  });
847
1073
 
848
1074
  if (args.operation.hasRequestBody()) {
849
- const requestBody = args.operation.getRequestBody("application/json");
850
1075
  const requestBodyObject = args.operation.schema.requestBody;
1076
+ const contentTypes = Object.keys(requestBodyObject?.content || {});
1077
+ requestBodyContentType =
1078
+ contentTypes.find((contentType) =>
1079
+ contentType.toLowerCase().includes("application/json"),
1080
+ ) ||
1081
+ contentTypes.find((contentType) =>
1082
+ contentType.toLowerCase().includes("multipart/form-data"),
1083
+ ) ||
1084
+ contentTypes[0];
1085
+ const requestBody = requestBodyContentType
1086
+ ? args.operation.getRequestBody(requestBodyContentType)
1087
+ : undefined;
851
1088
  const bodySchema = (requestBody as any)?.schema as any;
852
1089
  bodyDescription =
853
1090
  requestBodyObject?.description ||
@@ -871,6 +1108,7 @@ function buildOpenApiRequestModel(args: {
871
1108
  requestSectionVariants,
872
1109
  bodyDescription,
873
1110
  bodyDefaultKind,
1111
+ requestBodyContentType,
874
1112
  };
875
1113
  }
876
1114
 
@@ -0,0 +1,133 @@
1
+ export interface OpenApiResponseContentEntry {
2
+ contentType: string;
3
+ schema: any;
4
+ }
5
+
6
+ export interface OpenApiBinaryResponseSummary {
7
+ title: string;
8
+ description: string;
9
+ contentTypes: string[];
10
+ snippetText: string;
11
+ }
12
+
13
+ function getBaseContentType(contentType: string): string {
14
+ return String(contentType || "").split(";")[0].trim().toLowerCase();
15
+ }
16
+
17
+ function isMediaContentType(contentType: string): boolean {
18
+ const baseContentType = getBaseContentType(contentType);
19
+ return (
20
+ baseContentType.startsWith("audio/") ||
21
+ baseContentType.startsWith("image/") ||
22
+ baseContentType.startsWith("video/")
23
+ );
24
+ }
25
+
26
+ function getMediaKind(contentType: string): "audio" | "image" | "video" | null {
27
+ const baseContentType = getBaseContentType(contentType);
28
+ if (baseContentType.startsWith("audio/")) return "audio";
29
+ if (baseContentType.startsWith("image/")) return "image";
30
+ if (baseContentType.startsWith("video/")) return "video";
31
+ return null;
32
+ }
33
+
34
+ function isKnownBinaryContentType(contentType: string): boolean {
35
+ const baseContentType = getBaseContentType(contentType);
36
+ return (
37
+ isMediaContentType(baseContentType) ||
38
+ baseContentType === "application/octet-stream" ||
39
+ baseContentType === "application/pdf" ||
40
+ baseContentType === "application/zip" ||
41
+ baseContentType === "application/gzip"
42
+ );
43
+ }
44
+
45
+ function isBinarySchema(schema: any, seen = new Set<any>()): boolean {
46
+ if (!schema || typeof schema !== "object") return false;
47
+ if (seen.has(schema)) return false;
48
+
49
+ const nextSeen = new Set(seen);
50
+ nextSeen.add(schema);
51
+
52
+ if (schema.type === "string" && schema.format === "binary") return true;
53
+
54
+ if (Array.isArray(schema.oneOf) || Array.isArray(schema.anyOf)) {
55
+ const variants = Array.isArray(schema.oneOf) ? schema.oneOf : schema.anyOf;
56
+ return variants.some((variant: any) => isBinarySchema(variant, nextSeen));
57
+ }
58
+
59
+ if (Array.isArray(schema.allOf)) {
60
+ return schema.allOf.some((part: any) => isBinarySchema(part, nextSeen));
61
+ }
62
+
63
+ return false;
64
+ }
65
+
66
+ function formatContentTypes(contentTypes: string[]): string {
67
+ if (contentTypes.length <= 1) return contentTypes[0] || "binary content";
68
+ if (contentTypes.length === 2) return `${contentTypes[0]} or ${contentTypes[1]}`;
69
+ return `${contentTypes.slice(0, -1).join(", ")}, or ${contentTypes.at(-1)}`;
70
+ }
71
+
72
+ function getBinaryTitle(contentTypes: string[]): string {
73
+ const mediaKinds = Array.from(
74
+ new Set(
75
+ contentTypes
76
+ .map((contentType) => getMediaKind(contentType))
77
+ .filter((kind): kind is "audio" | "image" | "video" => Boolean(kind)),
78
+ ),
79
+ );
80
+
81
+ if (mediaKinds.length === 1) {
82
+ const [kind] = mediaKinds;
83
+ return `Binary ${kind} response`;
84
+ }
85
+
86
+ if (mediaKinds.length > 1) return "Binary media response";
87
+ return "Binary file response";
88
+ }
89
+
90
+ export function getResponseContentEntries(
91
+ response: any,
92
+ ): OpenApiResponseContentEntry[] {
93
+ const content = response?.content;
94
+ if (!content || typeof content !== "object") return [];
95
+
96
+ return Object.entries(content).map(([contentType, mediaType]: [string, any]) => ({
97
+ contentType,
98
+ schema: mediaType?.schema,
99
+ }));
100
+ }
101
+
102
+ export function getBinaryResponseSummary(
103
+ response: any,
104
+ ): OpenApiBinaryResponseSummary | undefined {
105
+ const entries = getResponseContentEntries(response);
106
+ const binaryEntries = entries.filter((entry) => {
107
+ return (
108
+ isBinarySchema(entry.schema) || isKnownBinaryContentType(entry.contentType)
109
+ );
110
+ });
111
+
112
+ if (binaryEntries.length === 0) return undefined;
113
+
114
+ const mediaEntries = binaryEntries.filter((entry) =>
115
+ isMediaContentType(entry.contentType),
116
+ );
117
+ const displayEntries = mediaEntries.length > 0 ? mediaEntries : binaryEntries;
118
+ const contentTypes = Array.from(
119
+ new Set(displayEntries.map((entry) => getBaseContentType(entry.contentType))),
120
+ );
121
+ const title = getBinaryTitle(contentTypes);
122
+ const formattedTypes = formatContentTypes(contentTypes);
123
+ const description = `Returns ${formattedTypes} as raw response bytes. The body is not a structured JSON object.`;
124
+ const contentTypeLabel =
125
+ contentTypes.length === 1 ? "Content-Type" : "Content-Types";
126
+
127
+ return {
128
+ title,
129
+ description,
130
+ contentTypes,
131
+ snippetText: `${title}\n${contentTypeLabel}: ${contentTypes.join(", ")}\n\nThe response body is raw bytes, not a JSON object.`,
132
+ };
133
+ }