Skip to content

Commit

Permalink
fix: support OData Batch Processing (#507)
Browse files Browse the repository at this point in the history
  • Loading branch information
AnWeber committed Jul 24, 2023
1 parent db6c927 commit 0324780
Show file tree
Hide file tree
Showing 14 changed files with 147 additions and 10 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## [6.6.2] (2023-07-24)

### Fixes

- support OData Batch Processing (#507)

## [6.6.1] (2023-07-23)

### Fixes
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"license": "MIT",
"publisher": "AnWeber",
"description": "HTTP/REST CLI Client for *.http files",
"version": "6.6.1",
"version": "6.6.2",
"homepage": "https://github.com/AnWeber/httpyac",
"repository": {
"type": "git",
Expand Down
1 change: 1 addition & 0 deletions src/models/contentType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export interface ContentType {
mimeType: string;
contentType: string;
charset?: string | undefined;
boundary?: string | undefined;
}
1 change: 1 addition & 0 deletions src/models/parserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ export interface ParserContext {
httpFile: HttpFile;
data: ParserContextData;
httpFileStore: HttpFileStore;
forceRegionDelimiter?: boolean;
}
12 changes: 12 additions & 0 deletions src/plugins/core/metaData/forceRegionDelimiterMetaDataHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as models from '../../../models';

export function forceRegionDelimiterMetaDataHandler(
type: string,
_value: string | undefined,
context: models.ParserContext
) {
if (type === 'forceRegionDelimiter') {
context.forceRegionDelimiter = true;
}
return false;
}
2 changes: 2 additions & 0 deletions src/plugins/core/metaData/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { HttpyacHooksApi } from '../../../models';
import { DefaultMetaDataHandler } from './defaultMetaDataHandler';
import { forceRegionDelimiterMetaDataHandler } from './forceRegionDelimiterMetaDataHandler';
import { importMetaDataHandler } from './importMetaDataHandler';
import { jwtMetaDataHandler } from './jwtMetaDataHandler';
import { keepStreamingMetaDataHandler } from './keepStreamingMetaDataHandler';
Expand Down Expand Up @@ -28,6 +29,7 @@ export function initParseMetData(api: HttpyacHooksApi) {
api.hooks.parseMetaData.addHook('responseRef', responseRefMetaDataHandler);
api.hooks.parseMetaData.addHook('sleep', sleepMetaDataHandler);
api.hooks.parseMetaData.addHook('verbose', verboseMetaDataHandler);
api.hooks.parseMetaData.addHook('forceRegionDelimiter', forceRegionDelimiterMetaDataHandler);

api.hooks.parseMetaData.addInterceptor(new DefaultMetaDataHandler());
}
3 changes: 3 additions & 0 deletions src/plugins/core/parse/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as models from '../../../models';
import { parseComment } from './commentHttpRegionParser';
import { parseMetaData } from './metaHttpRegionParser';
import { MultipartMixedInterceptor } from './multipartMixedInterceptor';
import { parseOutputRedirection } from './outputRedirectionHttpRegionParser';
import { parseRequestBody } from './requestBodyHttpRegionParser';
import { parseHttpRequestLine } from './requestHttpRegionParser';
Expand All @@ -17,4 +18,6 @@ export function initParseHook(api: models.HttpyacHooksApi) {
api.hooks.parse.addHook('responseRef', parseResponseRef);
api.hooks.parse.addHook('response', parseResponse);
api.hooks.parse.addHook('requestBody', parseRequestBody);

api.hooks.parse.addInterceptor(new MultipartMixedInterceptor());
}
32 changes: 32 additions & 0 deletions src/plugins/core/parse/multipartMixedInterceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as models from '../../../models';
import * as utils from '../../../utils';
import { getRequestBody } from './requestBodyHttpRegionParser';
import { HookInterceptor, HookTriggerContext } from 'hookpoint';

export class MultipartMixedInterceptor
implements HookInterceptor<[models.getHttpLineGenerator, models.ParserContext], undefined>
{
get id() {
return 'multipart/mixed';
}

async beforeLoop(
hookContext: HookTriggerContext<[models.getHttpLineGenerator, models.ParserContext], undefined>
): Promise<boolean | undefined> {
const context = hookContext.args[1];

if (context.httpRegion.request && utils.isMimeTypeMultiPartMixed(context.httpRegion.request.contentType)) {
if (context.forceRegionDelimiter === undefined) {
context.forceRegionDelimiter = true;
} else if (context.forceRegionDelimiter) {
const boundary = context.httpRegion.request.contentType?.boundary || '';
const lastline = getRequestBody(context).rawBody.slice().pop();
if (utils.isString(lastline) && lastline.includes(`--${boundary}--`)) {
context.forceRegionDelimiter = false;
}
}
}

return true;
}
}
2 changes: 1 addition & 1 deletion src/plugins/core/parse/requestBodyHttpRegionParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export async function parseRequestBody(
return false;
}

function getRequestBody(context: models.ParserContext) {
export function getRequestBody(context: models.ParserContext) {
let result = context.data.request_body;
if (!result) {
result = {
Expand Down
1 change: 1 addition & 0 deletions src/store/parser/closeHttpRegion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export async function closeHttpRegion(parserContext: models.ParserContext): Prom
parserContext.httpRegion.symbol.name = utils.getDisplayName(httpRegion);
parserContext.httpRegion.symbol.description = utils.getRegionDescription(httpRegion);
parserContext.httpFile.httpRegions.push(parserContext.httpRegion);
delete parserContext.forceRegionDelimiter;
}
67 changes: 67 additions & 0 deletions src/test/request/odata.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { parseHttp, sendHttpFile, initFileProvider } from '../testUtils';
import { getLocal } from 'mockttp';

describe('request.odata', () => {
const localServer = getLocal();
beforeAll(async () => await localServer.start());
afterAll(async () => await localServer.stop());

it('batch processing', async () => {
initFileProvider();
const mockedEndpoints = await localServer.forPost('/$batch').thenReply(200);

const body = `--batch_36522ad7-fc75-4b56-8c71-56071383e77b
Content-Type: application/http
Content-Transfer-Encoding:binary
GET /service/Customers('ALFKI') HTTP/1.1
--batch_36522ad7-fc75-4b56-8c71-56071383e77b
Content-Type: multipart/mixed; boundary=changeset_77162fcd-b8da-41ac-a9f8-9357efbbd621
Content-Length: ###
--changeset_77162fcd-b8da-41ac-a9f8-9357efbbd621
Content-Type: application/http
Content-Transfer-Encoding: binary
POST /service/Customers HTTP/1.1
Host: host
Content-Type: application/atom+xml;type=entry
Content-Length: ###
<AtomPub representation of a new Customer>
--changeset_77162fcd-b8da-41ac-a9f8-9357efbbd621
Content-Type: application/http
Content-Transfer-Encoding:binary
PUT /service/Customers('ALFKI') HTTP/1.1
Host: host
Content-Type: application/json
If-Match: xxxxx
Content-Length: ###
<JSON representation of Customer ALFKI>
--changeset_77162fcd-b8da-41ac-a9f8-9357efbbd621--
--batch_36522ad7-fc75-4b56-8c71-56071383e77b--`;

const httpFile = await parseHttp(
`
POST /$batch
Content-Type: multipart/mixed; boundary=batch_36522ad7-fc75-4b56-8c71-56071383e77b
${body}
`
);

expect(httpFile.httpRegions.length).toBe(1);
await sendHttpFile(httpFile, {
host: `http://localhost:${localServer.port}`,
});

const requests = await mockedEndpoints.getSeenRequests();
expect(await requests[0].body.getText()).toBe(body);
});
});
7 changes: 6 additions & 1 deletion src/utils/mimeTypeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { ContentType } from '../models';
export function parseMimeType(contentType: string): ContentType {
const [mimeType, ...parameters] = contentType.split(';').map(v => v.trim());
const charset = parameters.find(p => p.startsWith('charset='))?.split('=')[1];
return { mimeType, contentType, charset };
const boundary = parameters.find(p => p.startsWith('boundary='))?.split('=')[1];
return { mimeType, contentType, charset, boundary };
}

export function isMimeTypeJSON(contentType: ContentType | undefined): boolean {
Expand Down Expand Up @@ -41,6 +42,10 @@ export function isMimeTypeMultiPartFormData(contentType: ContentType | undefined
return contentType?.mimeType === 'multipart/form-data';
}

export function isMimeTypeMultiPartMixed(contentType: ContentType | undefined): boolean {
return contentType?.mimeType === 'multipart/mixed';
}

export function isMimeTypeNewlineDelimitedJSON(contentType: ContentType | undefined): boolean {
return contentType?.mimeType === 'application/x-ndjson';
}
Expand Down
17 changes: 12 additions & 5 deletions src/utils/parseRequestClientFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function parseRequestLineFactory(requestContext: RequestParserContext) {
): Promise<models.HttpRegionParserResult> {
const lineReader = getLineReader();
const next = lineReader.next();
if (!next.done && isValidRequestLine(next.value, context.httpRegion, requestContext)) {
if (!next.done && isValidRequestLine(next.value, context, requestContext)) {
if (context.httpRegion.request) {
return {
endRegionLine: next.value.line - 1,
Expand Down Expand Up @@ -137,15 +137,22 @@ function getRequestParseLine(
return undefined;
}

function isValidRequestLine(httpLine: models.HttpLine, httpRegion: models.HttpRegion, context: RequestParserContext) {
function isValidRequestLine(
httpLine: models.HttpLine,
context: models.ParserContext,
requestContext: RequestParserContext
): boolean {
if (isStringEmpty(httpLine.textLine)) {
return false;
}
if (context.methodRegex.exec(httpLine.textLine)?.groups?.url) {
if (context.forceRegionDelimiter && !!context.httpRegion.request) {
return false;
}
if (requestContext.methodRegex.exec(httpLine.textLine)?.groups?.url) {
return true;
}
if (!httpRegion.request && context.protocolRegex) {
return context.protocolRegex.exec(httpLine.textLine)?.groups?.url;
if (!context?.httpRegion.request && requestContext.protocolRegex) {
return !!requestContext.protocolRegex.exec(httpLine.textLine)?.groups?.url;
}
return false;
}

0 comments on commit 0324780

Please sign in to comment.