diff --git a/bin/configs/typescript-fetch-allOf-readonly.yaml b/bin/configs/typescript-fetch-allOf-readonly.yaml new file mode 100644 index 000000000000..f872c77267e9 --- /dev/null +++ b/bin/configs/typescript-fetch-allOf-readonly.yaml @@ -0,0 +1,4 @@ +generatorName: typescript-fetch +outputDir: samples/client/petstore/typescript-fetch/builds/allOf-readonly +inputSpec: modules/openapi-generator/src/test/resources/3_0/allOf-readonly.yaml +templateDir: modules/openapi-generator/src/main/resources/typescript-fetch diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java index ea896fad5097..ef684023d350 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java @@ -189,6 +189,17 @@ private boolean isModelNeeded(Schema schema, Set visitedSchemas) { if (schema instanceof ComposedSchema) { // allOf, anyOf, oneOf ComposedSchema m = (ComposedSchema) schema; + + if (m.getAllOf() != null && m.getAllOf().size() == 1 && m.getReadOnly() != null && m.getReadOnly()) { + // Check if this composed schema only contains an allOf and a readOnly. + ComposedSchema c = new ComposedSchema(); + c.setAllOf(m.getAllOf()); + c.setReadOnly(true); + if (m.equals(c)) { + return isModelNeeded(m.getAllOf().get(0), visitedSchemas); + } + } + if (m.getAllOf() != null && !m.getAllOf().isEmpty()) { // check to ensure at least of the allOf item is model for (Schema inner : m.getAllOf()) { diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchModelTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchModelTest.java index e73a0988bbb3..259b7f55c917 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchModelTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchModelTest.java @@ -36,9 +36,11 @@ import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Locale; +import java.util.Map; /* import static io.swagger.codegen.CodegenConstants.IS_ENUM_EXT_NAME; @@ -458,4 +460,14 @@ public void testWithoutNullSafeAdditionalProps() { Assert.assertEquals(codegen.getTypeDeclaration(model), "{ [key: string]: string; }"); } + + @Test(description = "Don't generate new schemas for readonly references") + public void testNestedReadonlySchemas() { + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/allOf-readonly.yaml"); + final DefaultCodegen codegen = new TypeScriptFetchClientCodegen(); + codegen.processOpts(); + codegen.setOpenAPI(openAPI); + final Map schemaBefore = openAPI.getComponents().getSchemas(); + Assert.assertEquals(schemaBefore.keySet(), Sets.newHashSet("club", "owner")); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/allOf-readonly.yaml b/modules/openapi-generator/src/test/resources/3_0/allOf-readonly.yaml new file mode 100644 index 000000000000..643803b6fd67 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/allOf-readonly.yaml @@ -0,0 +1,40 @@ +openapi: 3.0.1 +info: + version: 1.0.0 + title: Example + license: + name: MIT +servers: + - url: http://api.example.xyz/v1 +paths: + /person/display/{personId}: + get: + parameters: + - name: personId + in: path + required: true + description: The id of the person to retrieve + schema: + type: string + operationId: list + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/club" +components: + schemas: + club: + properties: + owner: + allOf: + - $ref: '#/components/schemas/owner' + readOnly: true + + owner: + properties: + name: + type: string + maxLength: 255 \ No newline at end of file diff --git a/samples/client/petstore/typescript-fetch/builds/allOf-readonly/.openapi-generator-ignore b/samples/client/petstore/typescript-fetch/builds/allOf-readonly/.openapi-generator-ignore new file mode 100644 index 000000000000..7484ee590a38 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/allOf-readonly/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/samples/client/petstore/typescript-fetch/builds/allOf-readonly/.openapi-generator/FILES b/samples/client/petstore/typescript-fetch/builds/allOf-readonly/.openapi-generator/FILES new file mode 100644 index 000000000000..4036fef51863 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/allOf-readonly/.openapi-generator/FILES @@ -0,0 +1,7 @@ +apis/DefaultApi.ts +apis/index.ts +index.ts +models/Club.ts +models/Owner.ts +models/index.ts +runtime.ts diff --git a/samples/client/petstore/typescript-fetch/builds/allOf-readonly/.openapi-generator/VERSION b/samples/client/petstore/typescript-fetch/builds/allOf-readonly/.openapi-generator/VERSION new file mode 100644 index 000000000000..66672d4e9d31 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/allOf-readonly/.openapi-generator/VERSION @@ -0,0 +1 @@ +6.1.0-SNAPSHOT \ No newline at end of file diff --git a/samples/client/petstore/typescript-fetch/builds/allOf-readonly/apis/DefaultApi.ts b/samples/client/petstore/typescript-fetch/builds/allOf-readonly/apis/DefaultApi.ts new file mode 100644 index 000000000000..416fd54366a8 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/allOf-readonly/apis/DefaultApi.ts @@ -0,0 +1,62 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Example + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import * as runtime from '../runtime'; +import type { + Club, +} from '../models'; +import { + ClubFromJSON, + ClubToJSON, +} from '../models'; + +export interface ListRequest { + personId: string; +} + +/** + * + */ +export class DefaultApi extends runtime.BaseAPI { + + /** + */ + async listRaw(requestParameters: ListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters.personId === null || requestParameters.personId === undefined) { + throw new runtime.RequiredError('personId','Required parameter requestParameters.personId was null or undefined when calling list.'); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + const response = await this.request({ + path: `/person/display/{personId}`.replace(`{${"personId"}}`, encodeURIComponent(String(requestParameters.personId))), + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => ClubFromJSON(jsonValue)); + } + + /** + */ + async list(requestParameters: ListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.listRaw(requestParameters, initOverrides); + return await response.value(); + } + +} diff --git a/samples/client/petstore/typescript-fetch/builds/allOf-readonly/apis/index.ts b/samples/client/petstore/typescript-fetch/builds/allOf-readonly/apis/index.ts new file mode 100644 index 000000000000..69c44c00fa0d --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/allOf-readonly/apis/index.ts @@ -0,0 +1,3 @@ +/* tslint:disable */ +/* eslint-disable */ +export * from './DefaultApi'; diff --git a/samples/client/petstore/typescript-fetch/builds/allOf-readonly/index.ts b/samples/client/petstore/typescript-fetch/builds/allOf-readonly/index.ts new file mode 100644 index 000000000000..be9d1edeefeb --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/allOf-readonly/index.ts @@ -0,0 +1,5 @@ +/* tslint:disable */ +/* eslint-disable */ +export * from './runtime'; +export * from './apis'; +export * from './models'; diff --git a/samples/client/petstore/typescript-fetch/builds/allOf-readonly/models/Club.ts b/samples/client/petstore/typescript-fetch/builds/allOf-readonly/models/Club.ts new file mode 100644 index 000000000000..d2e709009000 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/allOf-readonly/models/Club.ts @@ -0,0 +1,71 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Example + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +import type { Owner } from './Owner'; +import { + OwnerFromJSON, + OwnerFromJSONTyped, + OwnerToJSON, +} from './Owner'; + +/** + * + * @export + * @interface Club + */ +export interface Club { + /** + * + * @type {Owner} + * @memberof Club + */ + readonly owner?: Owner; +} + +/** + * Check if a given object implements the Club interface. + */ +export function instanceOfClub(value: object): boolean { + let isInstance = true; + + return isInstance; +} + +export function ClubFromJSON(json: any): Club { + return ClubFromJSONTyped(json, false); +} + +export function ClubFromJSONTyped(json: any, ignoreDiscriminator: boolean): Club { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'owner': !exists(json, 'owner') ? undefined : OwnerFromJSON(json['owner']), + }; +} + +export function ClubToJSON(value?: Club | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + }; +} + diff --git a/samples/client/petstore/typescript-fetch/builds/allOf-readonly/models/Owner.ts b/samples/client/petstore/typescript-fetch/builds/allOf-readonly/models/Owner.ts new file mode 100644 index 000000000000..70542125af99 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/allOf-readonly/models/Owner.ts @@ -0,0 +1,65 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Example + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * + * @export + * @interface Owner + */ +export interface Owner { + /** + * + * @type {string} + * @memberof Owner + */ + name?: string; +} + +/** + * Check if a given object implements the Owner interface. + */ +export function instanceOfOwner(value: object): boolean { + let isInstance = true; + + return isInstance; +} + +export function OwnerFromJSON(json: any): Owner { + return OwnerFromJSONTyped(json, false); +} + +export function OwnerFromJSONTyped(json: any, ignoreDiscriminator: boolean): Owner { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'name': !exists(json, 'name') ? undefined : json['name'], + }; +} + +export function OwnerToJSON(value?: Owner | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'name': value.name, + }; +} + diff --git a/samples/client/petstore/typescript-fetch/builds/allOf-readonly/models/index.ts b/samples/client/petstore/typescript-fetch/builds/allOf-readonly/models/index.ts new file mode 100644 index 000000000000..c5f449053c2c --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/allOf-readonly/models/index.ts @@ -0,0 +1,4 @@ +/* tslint:disable */ +/* eslint-disable */ +export * from './Club'; +export * from './Owner'; diff --git a/samples/client/petstore/typescript-fetch/builds/allOf-readonly/runtime.ts b/samples/client/petstore/typescript-fetch/builds/allOf-readonly/runtime.ts new file mode 100644 index 000000000000..22b76e32c8d6 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/allOf-readonly/runtime.ts @@ -0,0 +1,407 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Example + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export const BASE_PATH = "http://api.example.xyz/v1".replace(/\/+$/, ""); + +export interface ConfigurationParameters { + basePath?: string; // override base path + fetchApi?: FetchAPI; // override for fetch implementation + middleware?: Middleware[]; // middleware to apply before/after fetch requests + queryParamsStringify?: (params: HTTPQuery) => string; // stringify function for query strings + username?: string; // parameter for basic security + password?: string; // parameter for basic security + apiKey?: string | ((name: string) => string); // parameter for apiKey security + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string | Promise); // parameter for oauth2 security + headers?: HTTPHeaders; //header params we want to use on every request + credentials?: RequestCredentials; //value for the credentials param we want to use on each request +} + +export class Configuration { + constructor(private configuration: ConfigurationParameters = {}) {} + + set config(configuration: Configuration) { + this.configuration = configuration; + } + + get basePath(): string { + return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH; + } + + get fetchApi(): FetchAPI | undefined { + return this.configuration.fetchApi; + } + + get middleware(): Middleware[] { + return this.configuration.middleware || []; + } + + get queryParamsStringify(): (params: HTTPQuery) => string { + return this.configuration.queryParamsStringify || querystring; + } + + get username(): string | undefined { + return this.configuration.username; + } + + get password(): string | undefined { + return this.configuration.password; + } + + get apiKey(): ((name: string) => string) | undefined { + const apiKey = this.configuration.apiKey; + if (apiKey) { + return typeof apiKey === 'function' ? apiKey : () => apiKey; + } + return undefined; + } + + get accessToken(): ((name?: string, scopes?: string[]) => string | Promise) | undefined { + const accessToken = this.configuration.accessToken; + if (accessToken) { + return typeof accessToken === 'function' ? accessToken : async () => accessToken; + } + return undefined; + } + + get headers(): HTTPHeaders | undefined { + return this.configuration.headers; + } + + get credentials(): RequestCredentials | undefined { + return this.configuration.credentials; + } +} + +export const DefaultConfig = new Configuration(); + +/** + * This is the base class for all generated API classes. + */ +export class BaseAPI { + + private middleware: Middleware[]; + + constructor(protected configuration = DefaultConfig) { + this.middleware = configuration.middleware; + } + + withMiddleware(this: T, ...middlewares: Middleware[]) { + const next = this.clone(); + next.middleware = next.middleware.concat(...middlewares); + return next; + } + + withPreMiddleware(this: T, ...preMiddlewares: Array) { + const middlewares = preMiddlewares.map((pre) => ({ pre })); + return this.withMiddleware(...middlewares); + } + + withPostMiddleware(this: T, ...postMiddlewares: Array) { + const middlewares = postMiddlewares.map((post) => ({ post })); + return this.withMiddleware(...middlewares); + } + + protected async request(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction): Promise { + const { url, init } = await this.createFetchParams(context, initOverrides); + const response = await this.fetchApi(url, init); + if (response.status >= 200 && response.status < 300) { + return response; + } + throw new ResponseError(response, 'Response returned an error code'); + } + + private async createFetchParams(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction) { + let url = this.configuration.basePath + context.path; + if (context.query !== undefined && Object.keys(context.query).length !== 0) { + // only add the querystring to the URL if there are query parameters. + // this is done to avoid urls ending with a "?" character which buggy webservers + // do not handle correctly sometimes. + url += '?' + this.configuration.queryParamsStringify(context.query); + } + + const headers = Object.assign({}, this.configuration.headers, context.headers); + Object.keys(headers).forEach(key => headers[key] === undefined ? delete headers[key] : {}); + + const initOverrideFn = + typeof initOverrides === "function" + ? initOverrides + : async () => initOverrides; + + const initParams = { + method: context.method, + headers, + body: context.body, + credentials: this.configuration.credentials, + }; + + const overridedInit: RequestInit = { + ...initParams, + ...(await initOverrideFn({ + init: initParams, + context, + })) + } + + const init: RequestInit = { + ...overridedInit, + body: + isFormData(overridedInit.body) || + overridedInit.body instanceof URLSearchParams || + isBlob(overridedInit.body) + ? overridedInit.body + : JSON.stringify(overridedInit.body), + }; + + return { url, init }; + } + + private fetchApi = async (url: string, init: RequestInit) => { + let fetchParams = { url, init }; + for (const middleware of this.middleware) { + if (middleware.pre) { + fetchParams = await middleware.pre({ + fetch: this.fetchApi, + ...fetchParams, + }) || fetchParams; + } + } + let response = undefined; + try { + response = await (this.configuration.fetchApi || fetch)(fetchParams.url, fetchParams.init); + } catch (e) { + for (const middleware of this.middleware) { + if (middleware.onError) { + response = await middleware.onError({ + fetch: this.fetchApi, + url: fetchParams.url, + init: fetchParams.init, + error: e, + response: response ? response.clone() : undefined, + }) || response; + } + } + if (response === undefined) { + if (e instanceof Error) { + throw new FetchError(e, 'The request failed and the interceptors did not return an alternative response'); + } else { + throw e; + } + } + } + for (const middleware of this.middleware) { + if (middleware.post) { + response = await middleware.post({ + fetch: this.fetchApi, + url: fetchParams.url, + init: fetchParams.init, + response: response.clone(), + }) || response; + } + } + return response; + } + + /** + * Create a shallow clone of `this` by constructing a new instance + * and then shallow cloning data members. + */ + private clone(this: T): T { + const constructor = this.constructor as any; + const next = new constructor(this.configuration); + next.middleware = this.middleware.slice(); + return next; + } +}; + +function isBlob(value: any): value is Blob { + return typeof Blob !== 'undefined' && value instanceof Blob +} + +function isFormData(value: any): value is FormData { + return typeof FormData !== "undefined" && value instanceof FormData +} + +export class ResponseError extends Error { + name: "ResponseError" = "ResponseError"; + constructor(public response: Response, msg?: string) { + super(msg); + } +} + +export class FetchError extends Error { + name: "FetchError" = "FetchError"; + constructor(public cause: Error, msg?: string) { + super(msg); + } +} + +export class RequiredError extends Error { + name: "RequiredError" = "RequiredError"; + constructor(public field: string, msg?: string) { + super(msg); + } +} + +export const COLLECTION_FORMATS = { + csv: ",", + ssv: " ", + tsv: "\t", + pipes: "|", +}; + +export type FetchAPI = WindowOrWorkerGlobalScope['fetch']; + +export type Json = any; +export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD'; +export type HTTPHeaders = { [key: string]: string }; +export type HTTPQuery = { [key: string]: string | number | null | boolean | Array | Set | HTTPQuery }; +export type HTTPBody = Json | FormData | URLSearchParams; +export type HTTPRequestInit = { headers?: HTTPHeaders; method: HTTPMethod; credentials?: RequestCredentials; body?: HTTPBody } +export type ModelPropertyNaming = 'camelCase' | 'snake_case' | 'PascalCase' | 'original'; + +export type InitOverrideFunction = (requestContext: { init: HTTPRequestInit, context: RequestOpts }) => Promise + +export interface FetchParams { + url: string; + init: RequestInit; +} + +export interface RequestOpts { + path: string; + method: HTTPMethod; + headers: HTTPHeaders; + query?: HTTPQuery; + body?: HTTPBody; +} + +export function exists(json: any, key: string) { + const value = json[key]; + return value !== null && value !== undefined; +} + +export function querystring(params: HTTPQuery, prefix: string = ''): string { + return Object.keys(params) + .map(key => querystringSingleKey(key, params[key], prefix)) + .filter(part => part.length > 0) + .join('&'); +} + +function querystringSingleKey(key: string, value: string | number | null | undefined | boolean | Array | Set | HTTPQuery, keyPrefix: string = ''): string { + const fullKey = keyPrefix + (keyPrefix.length ? `[${key}]` : key); + if (value instanceof Array) { + const multiValue = value.map(singleValue => encodeURIComponent(String(singleValue))) + .join(`&${encodeURIComponent(fullKey)}=`); + return `${encodeURIComponent(fullKey)}=${multiValue}`; + } + if (value instanceof Set) { + const valueAsArray = Array.from(value); + return querystringSingleKey(key, valueAsArray, keyPrefix); + } + if (value instanceof Date) { + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + } + if (value instanceof Object) { + return querystring(value as HTTPQuery, fullKey); + } + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`; +} + +export function mapValues(data: any, fn: (item: any) => any) { + return Object.keys(data).reduce( + (acc, key) => ({ ...acc, [key]: fn(data[key]) }), + {} + ); +} + +export function canConsumeForm(consumes: Consume[]): boolean { + for (const consume of consumes) { + if ('multipart/form-data' === consume.contentType) { + return true; + } + } + return false; +} + +export interface Consume { + contentType: string +} + +export interface RequestContext { + fetch: FetchAPI; + url: string; + init: RequestInit; +} + +export interface ResponseContext { + fetch: FetchAPI; + url: string; + init: RequestInit; + response: Response; +} + +export interface ErrorContext { + fetch: FetchAPI; + url: string; + init: RequestInit; + error: unknown; + response?: Response; +} + +export interface Middleware { + pre?(context: RequestContext): Promise; + post?(context: ResponseContext): Promise; + onError?(context: ErrorContext): Promise; +} + +export interface ApiResponse { + raw: Response; + value(): Promise; +} + +export interface ResponseTransformer { + (json: any): T; +} + +export class JSONApiResponse { + constructor(public raw: Response, private transformer: ResponseTransformer = (jsonValue: any) => jsonValue) {} + + async value(): Promise { + return this.transformer(await this.raw.json()); + } +} + +export class VoidApiResponse { + constructor(public raw: Response) {} + + async value(): Promise { + return undefined; + } +} + +export class BlobApiResponse { + constructor(public raw: Response) {} + + async value(): Promise { + return await this.raw.blob(); + }; +} + +export class TextApiResponse { + constructor(public raw: Response) {} + + async value(): Promise { + return await this.raw.text(); + }; +}