@contractkit/plugin-typescript 0.30.0 → 0.31.1

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.
@@ -11,6 +11,7 @@ import {
11
11
  opRequest,
12
12
  opMultiRequest,
13
13
  opResponse,
14
+ opResponseMulti,
14
15
  opOperation,
15
16
  opRoute,
16
17
  opRoot,
@@ -643,8 +644,8 @@ describe('generateOperation', () => {
643
644
  responses: [
644
645
  {
645
646
  statusCode: 200,
646
- contentType: 'application/json',
647
- bodyType: { kind: 'ref', name: 'Transfer' },
647
+ hasBlock: true,
648
+ bodies: [{ contentType: 'application/json', bodyType: { kind: 'ref', name: 'Transfer' } }],
648
649
  headers: [
649
650
  { name: 'preference-applied', optional: true, type: { kind: 'scalar', name: 'string' } },
650
651
  { name: 'etag', optional: false, type: { kind: 'scalar', name: 'string' } },
@@ -674,6 +675,8 @@ describe('generateOperation', () => {
674
675
  responses: [
675
676
  {
676
677
  statusCode: 204,
678
+ hasBlock: true,
679
+ bodies: [],
677
680
  headers: [{ name: 'x-deleted-at', optional: false, type: { kind: 'scalar', name: 'string' } }],
678
681
  },
679
682
  ],
@@ -694,6 +697,188 @@ describe('generateOperation', () => {
694
697
  expect(output).toContain('ctx.status = 200');
695
698
  });
696
699
 
700
+ // ─── Which statuses the service produces ─────────────────────────
701
+
702
+ describe('emitted-set dispatch', () => {
703
+ const artBodies = [
704
+ { contentType: 'image/png', bodyType: scalarType('binary') },
705
+ { contentType: 'image/jpeg', bodyType: scalarType('binary') },
706
+ ];
707
+
708
+ it('leaves the common success-plus-bodyless-errors operation alone', () => {
709
+ const root = opRoot([
710
+ opRoute('/pet', [
711
+ opOperation('get', {
712
+ responses: [opResponse(200, 'Pet', 'application/json'), opResponse(400), opResponse(404)],
713
+ }),
714
+ ]),
715
+ ]);
716
+ const output = generateOp(root);
717
+ expect(output).toContain('const result: Pet = await service.list();');
718
+ expect(output).toContain('ctx.status = 200;');
719
+ expect(output).toContain("ctx.type = 'application/json';");
720
+ expect(output).toContain('ctx.body = result;');
721
+ expect(output).not.toContain('switch (result.status)');
722
+ });
723
+
724
+ it('lets the service pick the mime when a status declares several', () => {
725
+ const root = opRoot([
726
+ opRoute('/art', [
727
+ opOperation('get', {
728
+ responses: [
729
+ opResponseMulti(200, artBodies, {
730
+ headers: [{ name: 'etag', optional: true, type: scalarType('string') }],
731
+ }),
732
+ opResponse(304),
733
+ ],
734
+ }),
735
+ ]),
736
+ ]);
737
+ const output = generateOp(root);
738
+ expect(output).toContain(
739
+ "const result: { contentType: 'image/png' | 'image/jpeg'; body: Buffer; headers: { etag?: string } } = await service.list();",
740
+ );
741
+ expect(output).toContain('ctx.status = 200;');
742
+ expect(output).toContain('ctx.type = result.contentType;');
743
+ expect(output).toContain('ctx.body = result.body;');
744
+ // The bare 304 is documentation — middleware produces it, not the service.
745
+ expect(output).not.toContain('switch (result.status)');
746
+ expect(output).not.toContain('304');
747
+ });
748
+
749
+ it('keeps contentType correlated with body when the mimes carry different types', () => {
750
+ const root = opRoot([
751
+ opRoute('/pet', [
752
+ opOperation('get', {
753
+ responses: [
754
+ opResponseMulti(200, [
755
+ { contentType: 'application/json', bodyType: refType('Pet') },
756
+ { contentType: 'text/csv', bodyType: scalarType('string') },
757
+ ]),
758
+ ],
759
+ }),
760
+ ]),
761
+ ]);
762
+ const output = generateOp(root);
763
+ expect(output).toContain(
764
+ "const result: { contentType: 'application/json'; body: Pet } | { contentType: 'text/csv'; body: string } = await service.list();",
765
+ );
766
+ });
767
+
768
+ it('switches on status when the service produces more than one', () => {
769
+ const root = opRoot([
770
+ opRoute('/art', [
771
+ opOperation('get', {
772
+ responses: [
773
+ opResponseMulti(200, artBodies, {
774
+ headers: [{ name: 'etag', optional: true, type: scalarType('string') }],
775
+ }),
776
+ opResponse(202, 'JobRef', 'application/json'),
777
+ opResponse(304),
778
+ opResponse(404),
779
+ ],
780
+ }),
781
+ ]),
782
+ ]);
783
+ const output = generateOp(root);
784
+ expect(output).toContain("| { status: 200; contentType: 'image/png' | 'image/jpeg'; body: Buffer; headers: { etag?: string } }");
785
+ expect(output).toContain("| { status: 202; contentType: 'application/json'; body: JobRef }");
786
+ expect(output).toContain('ctx.status = result.status;');
787
+ expect(output).toContain('switch (result.status) {');
788
+ expect(output).toContain(' case 200:');
789
+ expect(output).toContain(' case 202:');
790
+ // Neither the middleware-produced 304 nor the thrown 404 is a case.
791
+ expect(output).not.toContain('case 304:');
792
+ expect(output).not.toContain('case 404:');
793
+ });
794
+
795
+ it('returns a body-bearing error status rather than leaving it to be thrown', () => {
796
+ const root = opRoot([
797
+ opRoute('/pet', [
798
+ opOperation('get', {
799
+ responses: [
800
+ opResponse(200, 'Pet', 'application/json'),
801
+ opResponse(422, 'Problem', 'application/json'),
802
+ opResponse(404),
803
+ ],
804
+ }),
805
+ ]),
806
+ ]);
807
+ const output = generateOp(root);
808
+ expect(output).toContain("| { status: 200; contentType: 'application/json'; body: Pet }");
809
+ expect(output).toContain("| { status: 422; contentType: 'application/json'; body: Problem }");
810
+ expect(output).toContain(' case 422:');
811
+ });
812
+
813
+ it('puts a documented status back on the throw path', () => {
814
+ const root = opRoot([
815
+ opRoute('/pet', [
816
+ opOperation('get', {
817
+ responses: [
818
+ opResponse(200, 'Pet', 'application/json'),
819
+ { ...opResponse(422, 'Problem', 'application/json'), emit: 'documented' },
820
+ ],
821
+ }),
822
+ ]),
823
+ ]);
824
+ const output = generateOp(root);
825
+ expect(output).toContain('const result: Pet = await service.list();');
826
+ expect(output).not.toContain('switch (result.status)');
827
+ expect(output).not.toContain('422');
828
+ });
829
+
830
+ it('emits a bodyless status the service opts into with an empty block', () => {
831
+ const root = opRoot([
832
+ opRoute('/art', [
833
+ opOperation('get', {
834
+ responses: [opResponse(200, 'Art', 'application/json'), { statusCode: 304, bodies: [], hasBlock: true }],
835
+ }),
836
+ ]),
837
+ ]);
838
+ const output = generateOp(root);
839
+ expect(output).toContain("| { status: 200; contentType: 'application/json'; body: Art }");
840
+ expect(output).toContain('| { status: 304 }');
841
+ expect(output).toContain(' case 304:');
842
+ // Nothing to write for a bodyless member beyond the status itself.
843
+ expect(output).toMatch(/case 304:\n\s+break;/);
844
+ });
845
+
846
+ it('gives each status its own schema variable so two complex bodies cannot collide', () => {
847
+ const root = opRoot([
848
+ opRoute('/pet', [
849
+ opOperation('get', {
850
+ responses: [
851
+ opResponse(200, inlineObjectType([field('id', scalarType('int'))]), 'application/json'),
852
+ opResponse(422, inlineObjectType([field('detail', scalarType('string'))]), 'application/json'),
853
+ ],
854
+ }),
855
+ ]),
856
+ ]);
857
+ const output = generateOp(root);
858
+ expect(output).toContain('const result200Type = ');
859
+ expect(output).toContain('const result422Type = ');
860
+ });
861
+
862
+ it('writes only the headers belonging to the status that was returned', () => {
863
+ const root = opRoot([
864
+ opRoute('/art', [
865
+ opOperation('get', {
866
+ responses: [
867
+ opResponse(200, 'Art', 'application/json'),
868
+ {
869
+ ...opResponse(202, 'JobRef', 'application/json'),
870
+ headers: [{ name: 'retry-after', optional: false, type: scalarType('string') }],
871
+ },
872
+ ],
873
+ }),
874
+ ]),
875
+ ]);
876
+ const output = generateOp(root);
877
+ expect(output).toMatch(/case 202:\n\s+ctx\.set\('retry-after'/);
878
+ expect(output).not.toMatch(/case 200:\n\s+ctx\.set/);
879
+ });
880
+ });
881
+
697
882
  it('uses Output variant for result type when response model has format(output=...)', () => {
698
883
  const root = opRoot([
699
884
  opRoute('/auth/token', [
@@ -762,6 +947,32 @@ describe('generateOperation', () => {
762
947
  ]);
763
948
  expect(generateOp(root)).toContain('const result: Buffer[] = await service.list();');
764
949
  });
950
+
951
+ it('emits no _ZodBinary helper for a binary response body', () => {
952
+ // A response body is an annotation, not a schema — the handler never validates it,
953
+ // so declaring the helper would leave it unused and trip `noUnusedLocals`.
954
+ const root = opRoot([
955
+ opRoute('/art', [
956
+ opOperation('get', {
957
+ responses: [
958
+ opResponseMulti(200, [
959
+ { contentType: 'image/png', bodyType: scalarType('binary') },
960
+ { contentType: 'image/jpeg', bodyType: scalarType('binary') },
961
+ ]),
962
+ ],
963
+ }),
964
+ ]),
965
+ ]);
966
+ const output = generateOp(root);
967
+ expect(output).toContain('body: Buffer');
968
+ expect(output).not.toContain('_ZodBinary');
969
+ expect(output).not.toContain("import { z } from 'zod';");
970
+ });
971
+
972
+ it('still emits _ZodBinary when a request body actually validates binary', () => {
973
+ const root = opRoot([opRoute('/upload', [opOperation('post', { request: opRequest(scalarType('binary')) })])]);
974
+ expect(generateOp(root)).toContain('const _ZodBinary =');
975
+ });
765
976
  });
766
977
 
767
978
  describe('luxon imports cover every scalar that references a luxon class', () => {
@@ -770,13 +981,23 @@ describe('generateOperation', () => {
770
981
  expect(generateOp(root)).toContain("import { Duration } from 'luxon';");
771
982
  });
772
983
 
773
- it('imports Interval and emits the _ZodInterval helper for an interval body', () => {
774
- const root = opRoot([opRoute('/x', [opOperation('get', { responses: [opResponse(200, scalarType('interval'), 'application/json')] })])]);
984
+ it('imports Interval and emits the _ZodInterval helper for an interval request body', () => {
985
+ const root = opRoot([opRoute('/x', [opOperation('post', { request: opRequest(scalarType('interval')) })])]);
775
986
  const output = generateOp(root);
776
987
  expect(output).toContain("import { Interval } from 'luxon';");
777
988
  expect(output).toContain('const _ZodInterval =');
778
989
  });
779
990
 
991
+ it('emits neither for an interval response body, which is a plain string on the way out', () => {
992
+ // _ZodInterval transforms to an ISO string, so a response-side interval needs no
993
+ // schema and no luxon class — emitting either would leave both unused.
994
+ const root = opRoot([opRoute('/x', [opOperation('get', { responses: [opResponse(200, scalarType('interval'), 'application/json')] })])]);
995
+ const output = generateOp(root);
996
+ expect(output).toContain('const result: string = await service.list();');
997
+ expect(output).not.toContain('_ZodInterval');
998
+ expect(output).not.toContain('luxon');
999
+ });
1000
+
780
1001
  it('imports DateTime and Duration together when both are used', () => {
781
1002
  const root = opRoot([
782
1003
  opRoute('/x', [
@@ -14,6 +14,7 @@ import {
14
14
  hasPublicOperations,
15
15
  generateSdkPackageJson,
16
16
  generateSdkTsconfig,
17
+ generateErrorBodyAliases,
17
18
  } from '../src/codegen-sdk.js';
18
19
  import { collectPublicTypeNames } from '@contractkit/core';
19
20
  import { renderTsType, renderInputTsType } from '../src/ts-render.js';
@@ -25,6 +26,7 @@ import {
25
26
  opRequest,
26
27
  opMultiRequest,
27
28
  opResponse,
29
+ opResponseMulti,
28
30
  scalarType,
29
31
  refType,
30
32
  arrayType,
@@ -351,8 +353,8 @@ describe('generateSdk', () => {
351
353
  responses: [
352
354
  {
353
355
  statusCode: 200,
354
- contentType: 'application/json',
355
- bodyType: { kind: 'ref', name: 'Transfer' },
356
+ hasBlock: true,
357
+ bodies: [{ contentType: 'application/json', bodyType: { kind: 'ref', name: 'Transfer' } }],
356
358
  headers: [
357
359
  { name: 'preference-applied', optional: true, type: scalarType('string') },
358
360
  { name: 'etag', optional: false, type: scalarType('string') },
@@ -381,6 +383,8 @@ describe('generateSdk', () => {
381
383
  responses: [
382
384
  {
383
385
  statusCode: 204,
386
+ hasBlock: true,
387
+ bodies: [],
384
388
  headers: [{ name: 'x-deleted-at', optional: false, type: scalarType('string') }],
385
389
  },
386
390
  ],
@@ -649,7 +653,7 @@ describe('generateSdk', () => {
649
653
  expect(out).toContain('baseUrl: string');
650
654
  expect(out).toContain('headers?:');
651
655
  expect(out).toContain('fetch?: SdkFetch');
652
- expect(out).toContain('export class SdkError extends Error');
656
+ expect(out).toContain('export class SdkError<TBody = unknown> extends Error');
653
657
  expect(out).toContain('public readonly headers: Headers');
654
658
  expect(out).toContain('throw new SdkError(res.status, res.statusText, body, res.headers)');
655
659
  });
@@ -721,12 +725,186 @@ describe('generateSdk', () => {
721
725
  const root = opRoot([opRoute('/users', [opOperation('get', { responses: [opResponse(200, 'User', 'application/json')] })])]);
722
726
  const out = generateSdk(root);
723
727
  expect(out).toContain('export interface SdkOptions');
724
- expect(out).toContain('export class SdkError extends Error');
728
+ expect(out).toContain('export class SdkError<TBody = unknown> extends Error');
725
729
  expect(out).toContain('export type SdkFetch');
726
730
  expect(out).toContain('export function createSdkFetch(');
727
731
  expect(out).toContain('fetch?: SdkFetch');
728
732
  });
729
733
  });
734
+
735
+ // ─── What the client can receive ─────────────────────────────────────
736
+
737
+ describe('observable-set returns', () => {
738
+ const sharedOptions = { sdkOptionsPath: '/x/sdk-options.ts', outPath: '/x/client.ts' };
739
+ const artBodies = [
740
+ { contentType: 'image/png', bodyType: scalarType('binary') },
741
+ { contentType: 'image/jpeg', bodyType: scalarType('binary') },
742
+ ];
743
+
744
+ it('leaves the common success-plus-bodyless-errors method alone', () => {
745
+ const root = opRoot([
746
+ opRoute('/pet', [
747
+ opOperation('get', {
748
+ sdk: 'listPets',
749
+ responses: [opResponse(200, 'Pet', 'application/json'), opResponse(400), opResponse(404)],
750
+ }),
751
+ ]),
752
+ ]);
753
+ const out = generateSdk(root, sharedOptions);
754
+ expect(out).toContain('async listPets(): Promise<Pet> {');
755
+ expect(out).toContain('return await parseJson<Pet>(result);');
756
+ expect(out).not.toContain('expectStatuses');
757
+ expect(out).not.toContain('switch (result.status)');
758
+ });
759
+
760
+ it('tells the caller which mime came back when a status declares several', () => {
761
+ const root = opRoot([
762
+ opRoute('/art', [opOperation('get', { sdk: 'getArt', responses: [opResponseMulti(200, artBodies)] })]),
763
+ ]);
764
+ const out = generateSdk(root, sharedOptions);
765
+ expect(out).toContain("async getArt(): Promise<{ contentType: 'image/png' | 'image/jpeg'; data: Blob }> {");
766
+ expect(out).toContain("contentType: readContentType(result) as 'image/png' | 'image/jpeg'");
767
+ expect(out).toContain('data: await result.blob()');
768
+ expect(out).toContain("import { readContentType } from '/x/sdk-options.js'".replace('/x/', './'));
769
+ });
770
+
771
+ it('dispatches on the response content type when the mimes read differently', () => {
772
+ const root = opRoot([
773
+ opRoute('/pet', [
774
+ opOperation('get', {
775
+ sdk: 'getPet',
776
+ responses: [
777
+ opResponseMulti(200, [
778
+ { contentType: 'application/json', bodyType: refType('Pet') },
779
+ { contentType: 'text/csv', bodyType: scalarType('string') },
780
+ ]),
781
+ ],
782
+ }),
783
+ ]),
784
+ ]);
785
+ const out = generateSdk(root, sharedOptions);
786
+ expect(out).toContain("| { contentType: 'application/json'; data: Pet }");
787
+ expect(out).toContain("| { contentType: 'text/csv'; data: string }");
788
+ expect(out).toContain('switch (readContentType(result)) {');
789
+ expect(out).toContain("case 'text/csv':");
790
+ });
791
+
792
+ it('returns a union over every status a client can receive, including one middleware produces', () => {
793
+ const root = opRoot([
794
+ opRoute('/art', [
795
+ opOperation('get', {
796
+ sdk: 'getArt',
797
+ responses: [opResponse(200, 'Art', 'application/json'), opResponse(304), opResponse(404)],
798
+ }),
799
+ ]),
800
+ ]);
801
+ const out = generateSdk(root, sharedOptions);
802
+ expect(out).toContain("| { status: 200; contentType: 'application/json'; data: Art }");
803
+ expect(out).toContain('| { status: 304 }');
804
+ // The bare 404 still throws, so it is not a member.
805
+ expect(out).not.toContain('status: 404');
806
+ expect(out).toContain('switch (result.status) {');
807
+ });
808
+
809
+ it('stops throwing for a status declared as a value rather than an error', () => {
810
+ const root = opRoot([
811
+ opRoute('/pet', [
812
+ opOperation('get', {
813
+ sdk: 'getPet',
814
+ responses: [opResponse(200, 'Pet', 'application/json'), opResponse(422, 'Problem', 'application/json'), opResponse(404)],
815
+ }),
816
+ ]),
817
+ ]);
818
+ const out = generateSdk(root, sharedOptions);
819
+ expect(out).toContain('expectStatuses: [422]');
820
+ expect(out).toContain("| { status: 422; contentType: 'application/json'; data: Problem }");
821
+ expect(out).toContain('case 422:');
822
+ });
823
+
824
+ it('passes expectStatuses so a declared 304 does not surface as SdkError', () => {
825
+ const root = opRoot([
826
+ opRoute('/art', [opOperation('get', { sdk: 'getArt', responses: [opResponse(200, 'Art', 'application/json'), opResponse(304)] })]),
827
+ ]);
828
+ expect(generateSdk(root, sharedOptions)).toContain('expectStatuses: [304]');
829
+ });
830
+
831
+ it('types the throw path from the statuses that stay errors', () => {
832
+ const root = opRoot([
833
+ opRoute('/pet', [
834
+ opOperation('get', {
835
+ sdk: 'getPet',
836
+ responses: [
837
+ opResponse(200, 'Pet', 'application/json'),
838
+ { ...opResponse(404, refType('Problem'), 'application/json'), emit: 'documented' as const },
839
+ opResponse(500),
840
+ ],
841
+ }),
842
+ ]),
843
+ ]);
844
+ const out = generateSdk(root, sharedOptions);
845
+ expect(out).toContain('export type GetPetErrorBody = Problem;');
846
+ expect(out).toContain('/** @throws {SdkError<GetPetErrorBody>} on 404, 500 */');
847
+ // Documented, so it is not a return-union member.
848
+ expect(out).toContain('async getPet(): Promise<Pet> {');
849
+ });
850
+
851
+ it('emits no alias when the error statuses declare no body', () => {
852
+ const root = opRoot([
853
+ opRoute('/pet', [opOperation('get', { sdk: 'getPet', responses: [opResponse(200, 'Pet', 'application/json'), opResponse(404)] })]),
854
+ ]);
855
+ expect(generateSdk(root, sharedOptions)).not.toContain('ErrorBody');
856
+ });
857
+ });
858
+ });
859
+
860
+ describe('generateErrorBodyAliases', () => {
861
+ it('emits one alias per operation whose thrown statuses declare a body', () => {
862
+ const root = opRoot([
863
+ opRoute('/pet', [
864
+ opOperation('get', {
865
+ sdk: 'getPet',
866
+ responses: [
867
+ opResponse(200, 'Pet', 'application/json'),
868
+ { ...opResponse(404, refType('Problem'), 'application/json'), emit: 'documented' as const },
869
+ ],
870
+ }),
871
+ ]),
872
+ ]);
873
+ expect(generateErrorBodyAliases(root, {})).toEqual(['export type GetPetErrorBody = Problem;']);
874
+ });
875
+
876
+ it('merges the bodies of several thrown statuses into one union', () => {
877
+ const root = opRoot([
878
+ opRoute('/pet', [
879
+ opOperation('get', {
880
+ sdk: 'getPet',
881
+ responses: [
882
+ opResponse(200, 'Pet', 'application/json'),
883
+ { ...opResponse(404, refType('NotFound'), 'application/json'), emit: 'documented' as const },
884
+ { ...opResponse(500, refType('Problem'), 'application/json'), emit: 'documented' as const },
885
+ ],
886
+ }),
887
+ ]),
888
+ ]);
889
+ expect(generateErrorBodyAliases(root, {})).toEqual(['export type GetPetErrorBody = NotFound | Problem;']);
890
+ });
891
+
892
+ it('emits nothing when the thrown statuses are bodyless', () => {
893
+ const root = opRoot([
894
+ opRoute('/pet', [opOperation('get', { sdk: 'getPet', responses: [opResponse(200, 'Pet', 'application/json'), opResponse(404)] })]),
895
+ ]);
896
+ expect(generateErrorBodyAliases(root, {})).toEqual([]);
897
+ });
898
+
899
+ it('skips internal operations unless asked for them', () => {
900
+ const responses = [
901
+ opResponse(200, 'Pet', 'application/json'),
902
+ { ...opResponse(404, refType('Problem'), 'application/json'), emit: 'documented' as const },
903
+ ];
904
+ const root = opRoot([opRoute('/pet', [opOperation('get', { sdk: 'getPet', responses })], undefined, ['internal'])]);
905
+ expect(generateErrorBodyAliases(root, {})).toEqual([]);
906
+ expect(generateErrorBodyAliases(root, { includeInternal: true })).toHaveLength(1);
907
+ });
730
908
  });
731
909
 
732
910
  describe('deriveClientClassName', () => {
@@ -757,9 +935,9 @@ describe('generateSdkOptions', () => {
757
935
  expect(out).toContain('headers?:');
758
936
  expect(out).toContain('fetch?: SdkFetch');
759
937
  expect(out).toContain('requestIdFactory?: () => string');
760
- expect(out).toContain('export class SdkError extends Error');
938
+ expect(out).toContain('export class SdkError<TBody = unknown> extends Error');
761
939
  expect(out).toContain('public readonly status: number');
762
- expect(out).toContain('public readonly body: unknown');
940
+ expect(out).toContain('public readonly body: TBody');
763
941
  expect(out).toContain('public readonly headers: Headers');
764
942
  expect(out).toContain('throw new SdkError(res.status, res.statusText, body, res.headers)');
765
943
  expect(out).toContain('export type SdkFetch');
package/tests/helpers.ts CHANGED
@@ -135,7 +135,26 @@ export function opMultiRequest(entries: Array<[string, string | ContractTypeNode
135
135
  export function opResponse(statusCode: number, bodyType?: string | ContractTypeNode, contentType?: string): OpResponseNode {
136
136
  const bt: ContractTypeNode | undefined =
137
137
  bodyType === undefined ? undefined : typeof bodyType === 'string' ? parseBodyTypeString(bodyType) : bodyType;
138
- return { statusCode, contentType, bodyType: bt };
138
+ const bodies = bt === undefined ? [] : [{ contentType: contentType ?? 'application/json', bodyType: bt }];
139
+ return { statusCode, bodies, ...(bt !== undefined ? { hasBlock: true } : {}) };
140
+ }
141
+
142
+ /** A status declaring several mimes, as `200: { image/png: binary, image/jpeg: binary }` parses. */
143
+ export function opResponseMulti(
144
+ statusCode: number,
145
+ bodies: { contentType: string; bodyType: string | ContractTypeNode }[],
146
+ extra?: Partial<OpResponseNode>,
147
+ ): OpResponseNode {
148
+ const resolved = bodies.map(b => ({
149
+ contentType: b.contentType,
150
+ bodyType: typeof b.bodyType === 'string' ? parseBodyTypeString(b.bodyType) : b.bodyType,
151
+ }));
152
+ return {
153
+ statusCode,
154
+ bodies: resolved,
155
+ hasBlock: true,
156
+ ...extra,
157
+ };
139
158
  }
140
159
 
141
160
  function parseBodyTypeString(s: string): ContractTypeNode {