From 0d3f10b08e001120de23e58def7457d51202efad Mon Sep 17 00:00:00 2001 From: TheoryOfNekomata Date: Wed, 22 May 2024 21:44:16 +0800 Subject: [PATCH] Add HTTP-related exports Bring back content negotiation exports as well as define a new response type system. --- packages/core/src/backend/common.ts | 5 + packages/core/src/client/index.ts | 2 +- packages/core/src/common/app.ts | 26 +- packages/core/src/common/charset.ts | 11 + packages/core/src/common/endpoint.ts | 1 + packages/core/src/common/index.ts | 3 + packages/core/src/common/language.ts | 342 ++++++++++++++++++ packages/core/src/common/media-type.ts | 37 ++ packages/core/src/common/operation.ts | 5 - packages/core/src/extenders/http/response.ts | 64 ++++ .../core/src/extenders/http/status-codes.ts | 198 ++++++++++ packages/core/test/index.test.ts | 28 +- 12 files changed, 690 insertions(+), 32 deletions(-) create mode 100644 packages/core/src/common/charset.ts create mode 100644 packages/core/src/common/language.ts create mode 100644 packages/core/src/common/media-type.ts create mode 100644 packages/core/src/extenders/http/response.ts create mode 100644 packages/core/src/extenders/http/status-codes.ts diff --git a/packages/core/src/backend/common.ts b/packages/core/src/backend/common.ts index 0236707..85dd1e4 100644 --- a/packages/core/src/backend/common.ts +++ b/packages/core/src/backend/common.ts @@ -50,3 +50,8 @@ class BackendInstance implements Backend { export const backend = (params: BackendParams): Backend => { return new BackendInstance(params); }; + +export class PlainResponse { + constructor() { + } +} diff --git a/packages/core/src/client/index.ts b/packages/core/src/client/index.ts index c6e9557..3b30785 100644 --- a/packages/core/src/client/index.ts +++ b/packages/core/src/client/index.ts @@ -5,7 +5,7 @@ export interface ClientParams { fetch?: typeof fetch; } -export interface Client { +export interface Client { app: App; connect(params: ServiceParams): this; at(endpoint: TheEndpoint, params?: Record, unknown>): this; diff --git a/packages/core/src/common/app.ts b/packages/core/src/common/app.ts index 28b4bdf..72475ce 100644 --- a/packages/core/src/common/app.ts +++ b/packages/core/src/common/app.ts @@ -1,5 +1,5 @@ import {Endpoint, EndpointOperations} from './endpoint'; -import {BaseOperationParams, Operation} from './operation'; +import {Operation} from './operation'; export interface BaseAppState { endpoints: unknown; @@ -21,31 +21,11 @@ export interface App; endpoints: Set; - operation< - OperationName extends string, - OperationParams extends BaseOperationParams, - NewOperation extends Operation - >(newOperation: NewOperation): App< + operation(newOperation: NewOperation): App< AppName, { endpoints: AppState['endpoints'], - operations: keyof AppState['operations'] extends never ? { - [Key in NewOperation['name']]: ( - Exclude extends readonly string[] ? Exclude : never[] - ) - } : { - [Key in NewOperation['name'] | keyof AppState['operations']]: ( - AppState['operations'] extends Record - ? ( - AppState['operations'][Key] extends readonly string[] ? AppState['operations'][Key] : ( - Exclude extends readonly string[] ? Exclude : never[] - ) - ) - : ( - Exclude extends readonly string[] ? Exclude : never[] - ) - ); - } + operations: AppState['operations'] extends Array ? [...AppState['operations'], NewOperation['name']] : [NewOperation['name']] } >; endpoint(newEndpoint: EndpointOperations extends AppOperations ? NewEndpoint : never): App< diff --git a/packages/core/src/common/charset.ts b/packages/core/src/common/charset.ts new file mode 100644 index 0000000..e5086b4 --- /dev/null +++ b/packages/core/src/common/charset.ts @@ -0,0 +1,11 @@ +export interface Charset { + name: Name; + encode: (str: string) => Buffer; + decode: (buf: Buffer) => string; +} + +export const FALLBACK_CHARSET = { + encode: (str: string) => Buffer.from(str, 'utf-8'), + decode: (buf: Buffer) => buf.toString('utf-8'), + name: 'utf-8' as const, +} satisfies Charset; diff --git a/packages/core/src/common/endpoint.ts b/packages/core/src/common/endpoint.ts index df9e3d2..8f50739 100644 --- a/packages/core/src/common/endpoint.ts +++ b/packages/core/src/common/endpoint.ts @@ -17,6 +17,7 @@ export const serializeEndpointQueue = (endpointQueue: EndpointQueue) => { .map((s) => `/${s}`) .join(''); }) + .join('') }; export const parseToEndpointQueue = (urlWithoutBase: string, endpoints: Set) => { diff --git a/packages/core/src/common/index.ts b/packages/core/src/common/index.ts index e551ed8..7d1b355 100644 --- a/packages/core/src/common/index.ts +++ b/packages/core/src/common/index.ts @@ -1,6 +1,9 @@ export * from './app'; +export * from './charset'; export * from './data-source'; export * from './endpoint'; +export * from './language'; +export * from './media-type'; export * from './operation'; export * from './service'; export * as validation from 'valibot'; diff --git a/packages/core/src/common/language.ts b/packages/core/src/common/language.ts new file mode 100644 index 0000000..92542b7 --- /dev/null +++ b/packages/core/src/common/language.ts @@ -0,0 +1,342 @@ +export type MessageBody = string | string[] | (string | string[])[]; + +export const LANGUAGE_DEFAULT_ERROR_STATUS_MESSAGE_KEYS = [ + 'unableToInitializeResourceDataSource', + 'unableToFetchResourceCollection', + 'unableToFetchResource', + 'resourceIdNotGiven', + 'languageNotAcceptable', + 'characterSetNotAcceptable', + 'mediaTypeNotAcceptable', + 'methodNotAllowed', + 'urlNotFound', + 'badRequest', + 'deleteNonExistingResource', + 'unableToCreateResource', + 'unableToBindResourceDataSource', + 'unableToGenerateIdFromResourceDataSource', + 'unableToAssignIdFromResourceDataSource', + 'unableToEmplaceResource', + 'unableToSerializeResponse', + 'unableToEncodeResponse', + 'unableToDeleteResource', + 'unableToDeserializeResource', + 'unableToDecodeResource', + 'unableToDeserializeRequest', + 'patchNonExistingResource', + 'unableToPatchResource', + 'invalidResourcePatch', + 'invalidResourcePatchType', + 'invalidResource', + 'notImplemented', + 'internalServerError', + 'resourceNotFound', +] as const; + +export const LANGUAGE_DEFAULT_STATUS_MESSAGE_KEYS = [ + ...LANGUAGE_DEFAULT_ERROR_STATUS_MESSAGE_KEYS, + 'ok', + 'resourceCollectionFetched', + 'resourceCollectionQueried', + 'resourceFetched', + 'resourceDeleted', + 'resourcePatched', + 'resourceCreated', + 'resourceReplaced', + 'provideOptions', +] as const; + +export type LanguageDefaultStatusMessageKey = typeof LANGUAGE_DEFAULT_STATUS_MESSAGE_KEYS[number]; + +export interface LanguageStatusMessageMap extends Record {} + +export type LanguageDefaultErrorStatusMessageKey = typeof LANGUAGE_DEFAULT_ERROR_STATUS_MESSAGE_KEYS[number]; + +export interface LanguageBodyMap extends Record {} + +export interface Language { + name: Name, + statusMessages: LanguageStatusMessageMap, + bodies: LanguageBodyMap +} + +export const FALLBACK_LANGUAGE = { + name: 'en' as const, + statusMessages: { + unableToSerializeResponse: 'Unable To Serialize Response', + unableToEncodeResponse: 'Unable To Encode Response', + unableToBindResourceDataSource: 'Unable To Bind $RESOURCE Data Source', + unableToInitializeResourceDataSource: 'Unable To Initialize $RESOURCE Data Source', + unableToFetchResourceCollection: 'Unable To Fetch $RESOURCE Collection', + unableToFetchResource: 'Unable To Fetch $RESOURCE', + unableToDeleteResource: 'Unable To Delete $RESOURCE', + languageNotAcceptable: 'Language Not Acceptable', + characterSetNotAcceptable: 'Character Set Not Acceptable', + unableToDeserializeResource: 'Unable To Deserialize $RESOURCE', + unableToDecodeResource: 'Unable To Decode $RESOURCE', + mediaTypeNotAcceptable: 'Media Type Not Acceptable', + methodNotAllowed: 'Method Not Allowed', + urlNotFound: 'URL Not Found', + badRequest: 'Bad Request', + ok: 'OK', + provideOptions: 'Provide Options', + resourceCollectionFetched: '$RESOURCE Collection Fetched', + resourceCollectionQueried: '$RESOURCE Collection Queried', + resourceFetched: '$RESOURCE Fetched', + resourceNotFound: '$RESOURCE Not Found', + deleteNonExistingResource: 'Delete Non-Existing $RESOURCE', + resourceDeleted: '$RESOURCE Deleted', + unableToDeserializeRequest: 'Unable To Deserialize Request', + patchNonExistingResource: 'Patch Non-Existing $RESOURCE', + unableToPatchResource: 'Unable To Patch $RESOURCE', + invalidResourcePatch: 'Invalid $RESOURCE Patch', + invalidResourcePatchType: 'Invalid $RESOURCE Patch Type', + invalidResource: 'Invalid $RESOURCE', + resourcePatched: '$RESOURCE Patched', + resourceCreated: '$RESOURCE Created', + resourceReplaced: '$RESOURCE Replaced', + unableToGenerateIdFromResourceDataSource: 'Unable To Generate ID From $RESOURCE Data Source', + unableToAssignIdFromResourceDataSource: 'Unable To Assign ID From $RESOURCE Data Source', + unableToEmplaceResource: 'Unable To Emplace $RESOURCE', + resourceIdNotGiven: '$RESOURCE ID Not Given', + unableToCreateResource: 'Unable To Create $RESOURCE', + notImplemented: 'Not Implemented', + internalServerError: 'Internal Server Error', + }, + bodies: { + badRequest: [ + 'An invalid request has been made.', + [ + 'Check if the request body has all the required attributes for this endpoint.', + 'Check if the request body has only the valid attributes for this endpoint.', + 'Check if the request body matches the schema for the resource associated with this endpoint.', + 'Check if the request is appropriate for this endpoint.', + ], + ], + languageNotAcceptable: [ + 'The server could not process a response suitable for the client\'s provided language requirement.', + [ + 'Choose from the available languages on this service.', + 'Contact the administrator to provide localization for the client\'s given requirements.', + ], + ], + characterSetNotAcceptable: [ + 'The server could not process a response suitable for the client\'s provided character set requirement.', + [ + 'Choose from the available character sets on this service.', + 'Contact the administrator to provide localization for the client\'s given requirements.', + ], + ], + mediaTypeNotAcceptable: [ + 'The server could not process a response suitable for the client\'s provided media type requirement.', + [ + 'Choose from the available media types on this service.', + 'Contact the administrator to provide localization for the client\'s given requirements.', + ], + ], + deleteNonExistingResource: [ + 'The client has attempted to delete a resource that does not exist.', + [ + 'Ensure that the resource still exists.', + 'Ensure that the correct method is provided.', + ], + ], + internalServerError: [ + 'An unknown error has occurred within the service.', + [ + 'Try the request again at a later time.', + 'Contact the administrator if the service remains in a degraded or non-functional state.', + ], + ], + invalidResource: [ + 'The request has an invalid structure or is missing some attributes.', + [ + 'Check if the request body has all the required attributes for this endpoint.', + 'Check if the request body has only the valid attributes for this endpoint.', + 'Check if the request body matches the schema for the resource associated with this endpoint.', + ], + ], + invalidResourcePatch: [ + 'The request has an invalid patch data.', + [ + 'Check if the appropriate patch type is specified on the request data.', + 'Check if the request body has all the required attributes for this endpoint.', + 'Check if the request body has only the valid attributes for this endpoint.', + 'Check if the request body matches the schema for the resource associated with this endpoint.', + ], + ], + invalidResourcePatchType: [ + 'The request has an invalid or unsupported kind of patch data.', + [ + 'Check if the appropriate patch type is specified on the request data.', + 'Check if the request body has all the required attributes for this endpoint.', + 'Check if the request body has only the valid attributes for this endpoint.', + 'Check if the request body matches the schema for the resource associated with this endpoint.', + ], + ], + methodNotAllowed: [ + 'A request with an invalid or unsupported method has been made.', + [ + 'Check if the request method is appropriate for this endpoint.', + 'Check if the client is authorized to perform the method on this endpoint.', + ] + ], + notImplemented: [ + 'The service does not have any implementation for the accessed endpoint.', + [ + 'Try the request again at a later time.', + 'Contact the administrator if the service remains in a degraded or non-functional state.', + ], + ], + patchNonExistingResource: [ + 'The client has attempted to patch a resource that does not exist.', + [ + 'Ensure that the resource still exists.', + 'Ensure that the correct method is provided.', + ], + ], + resourceIdNotGiven: [ + 'The resource ID is not provided for the accessed endpoint.', + [ + 'Check if the resource ID is provided and valid in the URL.', + 'Check if the request method is appropriate for this endpoint.', + ], + ], + unableToAssignIdFromResourceDataSource: [ + 'The resource could not be assigned an ID from the associated data source.', + [ + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + unableToBindResourceDataSource: [ + 'The resource could not be associated from the data source.', + [ + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + unableToCreateResource: [ + 'An error has occurred on creating the resource.', + [ + 'Check if the request method is appropriate for this endpoint.', + 'Check if the request body has all the required attributes for this endpoint.', + 'Check if the request body has only the valid attributes for this endpoint.', + 'Check if the request body matches the schema for the resource associated with this endpoint.', + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + unableToDecodeResource: [ + 'The resource byte array could not be decoded for the provided character set.', + [ + 'Choose from the available character sets on this service.', + 'Contact the administrator to provide localization for the client\'s given requirements.', + ], + ], + unableToDeleteResource: [ + 'An error has occurred on deleting the resource.', + [ + 'Check if the request method is appropriate for this endpoint.', + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + unableToDeserializeRequest: [ + 'The decoded request byte array could not be deserialized for the provided media type.', + [ + 'Choose from the available media types on this service.', + 'Contact the administrator to provide localization for the client\'s given requirements.', + ], + ], + unableToDeserializeResource: [ + 'The decoded resource could not be deserialized for the provided media type.', + [ + 'Choose from the available media types on this service.', + 'Contact the administrator to provide localization for the client\'s given requirements.', + ], + ], + unableToEmplaceResource: [ + 'An error has occurred on emplacing the resource.', + [ + 'Check if the request method is appropriate for this endpoint.', + 'Check if the request body has all the required attributes for this endpoint.', + 'Check if the request body has only the valid attributes for this endpoint.', + 'Check if the request body matches the schema for the resource associated with this endpoint.', + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + unableToEncodeResponse: [ + 'The response data could not be encoded for the provided character set.', + [ + 'Choose from the available character sets on this service.', + 'Contact the administrator to provide localization for the client\'s given requirements.', + ], + ], + unableToFetchResource: [ + 'An error has occurred on fetching the resource.', + [ + 'Check if the request method is appropriate for this endpoint.', + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + unableToFetchResourceCollection: [ + 'An error has occurred on fetching the resource collection.', + [ + 'Check if the request method is appropriate for this endpoint.', + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + unableToGenerateIdFromResourceDataSource: [ + 'The associated data source for the resource could not produce an ID.', + [ + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + unableToInitializeResourceDataSource: [ + 'The associated data source for the resource could not be connected for usage.', + [ + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + unableToPatchResource: [ + 'An error has occurred on patching the resource.', + [ + 'Check if the request method is appropriate for this endpoint.', + 'Check if the request body has all the required attributes for this endpoint.', + 'Check if the request body has only the valid attributes for this endpoint.', + 'Check if the request body matches the schema for the resource associated with this endpoint.', + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + unableToSerializeResponse: [ + 'The response data could not be serialized for the provided media type.', + [ + 'Choose from the available media types on this service.', + 'Contact the administrator to provide localization for the client\'s given requirements.', + ], + ], + urlNotFound: [ + 'An endpoint in the provided URL could not be found.', + [ + 'Check if the request URL is correct.', + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + resourceNotFound: [ + 'The resource in the provided URL could not be found.', + [ + 'Check if the request URL is correct.', + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + }, +} satisfies Language; diff --git a/packages/core/src/common/media-type.ts b/packages/core/src/common/media-type.ts new file mode 100644 index 0000000..be412ae --- /dev/null +++ b/packages/core/src/common/media-type.ts @@ -0,0 +1,37 @@ +export interface MediaType< + Name extends string = string, + T extends object = object, + SerializeOpts extends {} = {}, + DeserializeOpts extends {} = {} +> { + name: Name; + serialize: (object: T, args?: SerializeOpts) => string; + deserialize: (s: string, args?: DeserializeOpts) => T; +} + +export const FALLBACK_MEDIA_TYPE = { + serialize: (obj: unknown) => JSON.stringify(obj), + deserialize: (str: string) => JSON.parse(str), + name: 'application/json' as const, +} satisfies MediaType; + +export const PATCH_CONTENT_TYPES = [ + 'application/merge-patch+json', + 'application/json-patch+json', +] as const; + +export type PatchContentType = typeof PATCH_CONTENT_TYPES[number]; + +export const getAcceptPostString = (mediaTypes: Map) => Array.from(mediaTypes.keys()) + .filter((t) => !PATCH_CONTENT_TYPES.includes(t as PatchContentType)) + .join(','); + +export const isTextMediaType = (mediaType: string) => ( + mediaType.startsWith('text/') + || [ + 'application/json', + 'application/xml', + 'application/x-www-form-urlencoded', + ...PATCH_CONTENT_TYPES, + ].includes(mediaType) +); diff --git a/packages/core/src/common/operation.ts b/packages/core/src/common/operation.ts index c2c4416..d875161 100644 --- a/packages/core/src/common/operation.ts +++ b/packages/core/src/common/operation.ts @@ -19,28 +19,23 @@ export type MethodWithExtensions = Method | typeof AVAILABLE_EXTENSION_METHODS[n export interface BaseOperationParams< Name extends string = string, Method extends MethodWithExtensions = MethodWithExtensions, - Args extends readonly string[] = readonly string[] > { name: Name; method?: Method; - args?: Args; } export interface Operation { name: Params['name']; method: Params['method']; - args: Params['args']; } class OperationInstance implements Operation { readonly name: Params['name']; readonly method: Params['method']; - readonly args: Params['args']; constructor(params: Params) { this.name = params.name; this.method = params.method ?? 'GET'; - this.args = params.args; } } diff --git a/packages/core/src/extenders/http/response.ts b/packages/core/src/extenders/http/response.ts new file mode 100644 index 0000000..f53823a --- /dev/null +++ b/packages/core/src/extenders/http/response.ts @@ -0,0 +1,64 @@ +import {ErrorStatusCode, isErrorStatusCode, StatusCode} from './status-codes'; +import http from 'http'; + +export interface Response { + statusCode: number; + statusMessage: string; + body?: Buffer; + res?: http.ServerResponse; +} + +interface ErrorResponse extends Error, Response {} + +interface HttpResponseConstructor { + new (...args: any[]): R; +} + +interface HttpResponseErrorConstructor extends HttpResponseConstructor { + new (message?: string, options?: ErrorOptions): R; +} + +interface HttpSuccessResponseConstructor extends HttpResponseConstructor { + new (response: Partial>, options?: Pick): R; +} + +interface HttpErrorOptions extends ErrorOptions { + res?: http.ServerResponse; + body?: Response['body']; +} + +export const HttpResponse = < + T extends StatusCode = StatusCode, + R extends Response = T extends ErrorStatusCode ? ErrorResponse : Response, +>(statusCode: T): T extends ErrorStatusCode ? HttpResponseErrorConstructor : HttpSuccessResponseConstructor => { + if (isErrorStatusCode(statusCode)) { + return class HttpErrorResponse extends Error implements ErrorResponse { + readonly statusMessage: string; + readonly statusCode: T; + readonly res?: http.ServerResponse; + readonly body?: Buffer; + + constructor(message?: string, options?: HttpErrorOptions) { + super(message, options); + this.name = this.statusMessage = message ?? ''; + this.statusCode = statusCode; + this.cause = options?.cause; + this.res = options?.res; + this.body = options?.body; + } + } as unknown as HttpResponseErrorConstructor; + } + + return class HttpSuccessResponse implements Response { + readonly statusMessage: string; + readonly statusCode: T; + readonly body?: Buffer; + readonly res?: http.ServerResponse; + constructor(params: Partial>, options?: Pick) { + this.statusCode = statusCode; + this.statusMessage = params.statusMessage ?? ''; + this.body = params.body; + this.res = options?.res; + } + } as unknown as HttpSuccessResponseConstructor; +}; diff --git a/packages/core/src/extenders/http/status-codes.ts b/packages/core/src/extenders/http/status-codes.ts new file mode 100644 index 0000000..7de10d7 --- /dev/null +++ b/packages/core/src/extenders/http/status-codes.ts @@ -0,0 +1,198 @@ +export const HTTP_STATUS_CONTINUE = 100 as const; +export const HTTP_STATUS_SWITCHING_PROTOCOLS = 101 as const; +export const HTTP_STATUS_PROCESSING = 102 as const; +export const HTTP_STATUS_EARLY_HINTS = 103 as const; + +export const IN_TRANSIT_STATUS_CODES = [ + HTTP_STATUS_CONTINUE, + HTTP_STATUS_SWITCHING_PROTOCOLS, + HTTP_STATUS_PROCESSING, + HTTP_STATUS_EARLY_HINTS, +] as const; + +export type InTransitStatusCode = typeof IN_TRANSIT_STATUS_CODES[number]; + +export const isInTransitStatusCode = (statusCode: StatusCode): statusCode is InTransitStatusCode => ( + IN_TRANSIT_STATUS_CODES.includes(statusCode as InTransitStatusCode) +); + +export const HTTP_STATUS_OK = 200 as const; +export const HTTP_STATUS_CREATED = 201 as const; +export const HTTP_STATUS_ACCEPTED = 202 as const; +export const HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION = 203 as const; +export const HTTP_STATUS_NO_CONTENT = 204 as const; +export const HTTP_STATUS_RESET_CONTENT = 205 as const; +export const HTTP_STATUS_PARTIAL_CONTENT = 206 as const; +export const HTTP_STATUS_MULTI_STATUS = 207 as const; +export const HTTP_STATUS_ALREADY_REPORTED = 208 as const; +export const HTTP_STATUS_IM_USED = 226 as const; +export const HTTP_STATUS_MULTIPLE_CHOICES = 300 as const; +export const HTTP_STATUS_MOVED_PERMANENTLY = 301 as const; +export const HTTP_STATUS_FOUND = 302 as const; +export const HTTP_STATUS_SEE_OTHER = 303 as const; +export const HTTP_STATUS_NOT_MODIFIED = 304 as const; +export const HTTP_STATUS_USE_PROXY = 305 as const; +export const HTTP_STATUS_TEMPORARY_REDIRECT = 307 as const; +export const HTTP_STATUS_PERMANENT_REDIRECT = 308 as const; + +export const RESOLVED_STATUS_CODES = [ + HTTP_STATUS_OK, + HTTP_STATUS_CREATED, + HTTP_STATUS_ACCEPTED, + HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION, + HTTP_STATUS_NO_CONTENT, + HTTP_STATUS_RESET_CONTENT, + HTTP_STATUS_PARTIAL_CONTENT, + HTTP_STATUS_MULTI_STATUS, + HTTP_STATUS_ALREADY_REPORTED, + HTTP_STATUS_IM_USED, +] as const; + +export type ResolvedStatusCode = typeof RESOLVED_STATUS_CODES[number]; + +export const isResolvedStatusCode = (statusCode: StatusCode): statusCode is ResolvedStatusCode => ( + RESOLVED_STATUS_CODES.includes(statusCode as ResolvedStatusCode) +); + +export const REDIRECT_STATUS_CODES = [ + HTTP_STATUS_MULTIPLE_CHOICES, + HTTP_STATUS_MOVED_PERMANENTLY, + HTTP_STATUS_FOUND, + HTTP_STATUS_SEE_OTHER, + HTTP_STATUS_NOT_MODIFIED, + HTTP_STATUS_USE_PROXY, + HTTP_STATUS_TEMPORARY_REDIRECT, + HTTP_STATUS_PERMANENT_REDIRECT, +] as const; + +export type RedirectStatusCode = typeof REDIRECT_STATUS_CODES[number]; + +export const isRedirectStatusCode = (statusCode: StatusCode): statusCode is RedirectStatusCode => ( + REDIRECT_STATUS_CODES.includes(statusCode as RedirectStatusCode) +); + +export const SUCCESS_STATUS_CODES = [ + ...RESOLVED_STATUS_CODES, + ...REDIRECT_STATUS_CODES, +]; + +export type SuccessStatusCode = typeof SUCCESS_STATUS_CODES[number]; + +export const isSuccessStatusCode = (statusCode: StatusCode): statusCode is SuccessStatusCode => ( + SUCCESS_STATUS_CODES.includes(statusCode as SuccessStatusCode) +); + +export const HTTP_STATUS_BAD_REQUEST = 400 as const; +export const HTTP_STATUS_UNAUTHORIZED = 401 as const; +export const HTTP_STATUS_PAYMENT_REQUIRED = 402 as const; +export const HTTP_STATUS_FORBIDDEN = 403 as const; +export const HTTP_STATUS_NOT_FOUND = 404 as const; +export const HTTP_STATUS_METHOD_NOT_ALLOWED = 405 as const; +export const HTTP_STATUS_NOT_ACCEPTABLE = 406 as const; +export const HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED = 407 as const; +export const HTTP_STATUS_REQUEST_TIMEOUT = 408 as const; +export const HTTP_STATUS_CONFLICT = 409 as const; +export const HTTP_STATUS_GONE = 410 as const; +export const HTTP_STATUS_LENGTH_REQUIRED = 411 as const; +export const HTTP_STATUS_PRECONDITION_FAILED = 412 as const; +export const HTTP_STATUS_PAYLOAD_TOO_LARGE = 413 as const; +export const HTTP_STATUS_URI_TOO_LONG = 414 as const; +export const HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE = 415 as const; +export const HTTP_STATUS_RANGE_NOT_SATISFIABLE = 416 as const; +export const HTTP_STATUS_EXPECTATION_FAILED = 417 as const; +export const HTTP_STATUS_TEAPOT = 418 as const; +export const HTTP_STATUS_MISDIRECTED_REQUEST = 421 as const; +export const HTTP_STATUS_UNPROCESSABLE_ENTITY = 422 as const; +export const HTTP_STATUS_LOCKED = 423 as const; +export const HTTP_STATUS_FAILED_DEPENDENCY = 424 as const; +export const HTTP_STATUS_UNORDERED_COLLECTION = 425 as const; +export const HTTP_STATUS_UPGRADE_REQUIRED = 426 as const; +export const HTTP_STATUS_PRECONDITION_REQUIRED = 428 as const; +export const HTTP_STATUS_TOO_MANY_REQUESTS = 429 as const; +export const HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE = 431 as const; +export const HTTP_STATUS_UNAVAILABLE_FOR_LEGAL_REASONS = 451 as const; + +export const CLIENT_ERROR_STATUS_CODES = [ + HTTP_STATUS_BAD_REQUEST, + HTTP_STATUS_UNAUTHORIZED, + HTTP_STATUS_PAYMENT_REQUIRED, + HTTP_STATUS_FORBIDDEN, + HTTP_STATUS_NOT_FOUND, + HTTP_STATUS_METHOD_NOT_ALLOWED, + HTTP_STATUS_NOT_ACCEPTABLE, + HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED, + HTTP_STATUS_REQUEST_TIMEOUT, + HTTP_STATUS_CONFLICT, + HTTP_STATUS_GONE, + HTTP_STATUS_LENGTH_REQUIRED, + HTTP_STATUS_PRECONDITION_FAILED, + HTTP_STATUS_PAYLOAD_TOO_LARGE, + HTTP_STATUS_URI_TOO_LONG, + HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, + HTTP_STATUS_RANGE_NOT_SATISFIABLE, + HTTP_STATUS_EXPECTATION_FAILED, + HTTP_STATUS_TEAPOT, + HTTP_STATUS_MISDIRECTED_REQUEST, + HTTP_STATUS_UNPROCESSABLE_ENTITY, + HTTP_STATUS_LOCKED, + HTTP_STATUS_FAILED_DEPENDENCY, + HTTP_STATUS_UNORDERED_COLLECTION, + HTTP_STATUS_UPGRADE_REQUIRED, + HTTP_STATUS_PRECONDITION_REQUIRED, + HTTP_STATUS_TOO_MANY_REQUESTS, + HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE, + HTTP_STATUS_UNAVAILABLE_FOR_LEGAL_REASONS, +] as const; + +export type ClientErrorStatusCode = typeof CLIENT_ERROR_STATUS_CODES[number]; + +export const isClientErrorStatusCode = (statusCode: StatusCode): statusCode is ClientErrorStatusCode => ( + CLIENT_ERROR_STATUS_CODES.includes(statusCode as ClientErrorStatusCode) +); + +export const HTTP_STATUS_INTERNAL_SERVER_ERROR = 500 as const; +export const HTTP_STATUS_NOT_IMPLEMENTED = 501 as const; +export const HTTP_STATUS_BAD_GATEWAY = 502 as const; +export const HTTP_STATUS_SERVICE_UNAVAILABLE = 503 as const; +export const HTTP_STATUS_GATEWAY_TIMEOUT = 504 as const; +export const HTTP_STATUS_HTTP_VERSION_NOT_SUPPORTED = 505 as const; +export const HTTP_STATUS_VARIANT_ALSO_NEGOTIATES = 506 as const; +export const HTTP_STATUS_INSUFFICIENT_STORAGE = 507 as const; +export const HTTP_STATUS_LOOP_DETECTED = 508 as const; +export const HTTP_STATUS_BANDWIDTH_LIMIT_EXCEEDED = 509 as const; +export const HTTP_STATUS_NOT_EXTENDED = 510 as const; +export const HTTP_STATUS_NETWORK_AUTHENTICATION_REQUIRED = 511 as const; + +export const SERVER_ERROR_STATUS_CODES = [ + HTTP_STATUS_INTERNAL_SERVER_ERROR, + HTTP_STATUS_NOT_IMPLEMENTED, + HTTP_STATUS_BAD_GATEWAY, + HTTP_STATUS_SERVICE_UNAVAILABLE, + HTTP_STATUS_GATEWAY_TIMEOUT, + HTTP_STATUS_HTTP_VERSION_NOT_SUPPORTED, + HTTP_STATUS_VARIANT_ALSO_NEGOTIATES, + HTTP_STATUS_INSUFFICIENT_STORAGE, + HTTP_STATUS_LOOP_DETECTED, + HTTP_STATUS_BANDWIDTH_LIMIT_EXCEEDED, + HTTP_STATUS_NOT_EXTENDED, + HTTP_STATUS_NETWORK_AUTHENTICATION_REQUIRED, +] as const; + +export type ServerErrorStatusCode = typeof SERVER_ERROR_STATUS_CODES[number]; + +export const isServerErrorStatusCode = (statusCode: StatusCode): statusCode is ServerErrorStatusCode => ( + SERVER_ERROR_STATUS_CODES.includes(statusCode as ServerErrorStatusCode) +); + +export const ERROR_STATUS_CODES = [ + ...CLIENT_ERROR_STATUS_CODES, + ...SERVER_ERROR_STATUS_CODES, +] as const; + +export type ErrorStatusCode = typeof ERROR_STATUS_CODES[number]; + +export const isErrorStatusCode = (statusCode: StatusCode): statusCode is ErrorStatusCode => ( + ERROR_STATUS_CODES.includes(statusCode as ErrorStatusCode) +); + +export type StatusCode = InTransitStatusCode | SuccessStatusCode | ErrorStatusCode; diff --git a/packages/core/test/index.test.ts b/packages/core/test/index.test.ts index 9b0cd77..f5baade 100644 --- a/packages/core/test/index.test.ts +++ b/packages/core/test/index.test.ts @@ -1,16 +1,34 @@ -import {describe, it, expect, beforeAll} from 'vitest'; -import {App, app, Endpoint, endpoint, Operation, operation, validation as v} from '../src/common'; +import {describe, it, expect, beforeAll, afterAll} from 'vitest'; +import {App, app, DataSource, Endpoint, endpoint, Operation, operation, validation as v} from '../src/common'; import {server} from '../src/extenders/http/backend'; import {Backend, backend, Server} from '../src/backend'; import {Client} from '../src/client'; import {client} from '../src/extenders/http/client'; +const op = operation({ + name: 'create' as const, + method: 'POST' as const, +}); + +const e = endpoint({ + name: 'e' as const, + schema: v.object({}), +}) + .can('create'); + +const a = app({ + name: 'foo' as const, +}) + .operation(op) + .endpoint(e); + describe('app', () => { let theApp: App; let theBackend: Backend; + let theClient: Client; + let theDataSource: DataSource; let theEndpoint: Endpoint; let theServer: Server; - let theClient: Client; let theOperation: Operation; beforeAll(async () => { @@ -61,6 +79,10 @@ describe('app', () => { .connect(connectionParams); }); + afterAll(() => { + theServer.close(); + }); + it('works', async () => { const response = await theClient .at(theEndpoint, { resourceId: 3 })