@squiz/dx-json-schema-lib 1.12.0-alpha.46 → 1.12.0-alpha.47
Sign up to get free protection for your applications and to get access to all the features.
- package/.npm/_logs/{2023-02-24T01_14_03_380Z-debug-0.log → 2023-02-24T04_47_40_972Z-debug-0.log} +10 -10
- package/lib/JsonValidationService.d.ts +33 -0
- package/lib/JsonValidationService.js +101 -1
- package/lib/JsonValidationService.js.map +1 -1
- package/lib/JsonValidationService.spec.js +168 -0
- package/lib/JsonValidationService.spec.js.map +1 -1
- package/lib/errors/JsonResolutionError.d.ts +5 -0
- package/lib/errors/JsonResolutionError.js +12 -0
- package/lib/errors/JsonResolutionError.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/jsonTypeResolution/arbitraryTypeResolution.d.ts +61 -0
- package/lib/jsonTypeResolution/arbitraryTypeResolution.js +61 -0
- package/lib/jsonTypeResolution/arbitraryTypeResolution.js.map +1 -0
- package/lib/jsonTypeResolution/arbitraryTypeResolution.spec.d.ts +1 -0
- package/lib/jsonTypeResolution/arbitraryTypeResolution.spec.js +100 -0
- package/lib/jsonTypeResolution/arbitraryTypeResolution.spec.js.map +1 -0
- package/lib/jsonTypeResolution/index.d.ts +76 -0
- package/lib/jsonTypeResolution/index.js +35 -0
- package/lib/jsonTypeResolution/index.js.map +1 -0
- package/lib/jsonTypeResolution/primitiveTypes.d.ts +10 -0
- package/lib/jsonTypeResolution/primitiveTypes.js +27 -0
- package/lib/jsonTypeResolution/primitiveTypes.js.map +1 -0
- package/lib/jsonTypeResolution/resolvableTypes.d.ts +12 -0
- package/lib/jsonTypeResolution/resolvableTypes.js +30 -0
- package/lib/jsonTypeResolution/resolvableTypes.js.map +1 -0
- package/package.json +4 -3
- package/src/JsonValidationService.spec.ts +280 -1
- package/src/JsonValidationService.ts +127 -1
- package/src/errors/JsonResolutionError.ts +5 -0
- package/src/index.ts +3 -0
- package/src/jsonTypeResolution/arbitraryTypeResolution.spec.ts +134 -0
- package/src/jsonTypeResolution/arbitraryTypeResolution.ts +100 -0
- package/src/jsonTypeResolution/index.ts +16 -0
- package/src/jsonTypeResolution/primitiveTypes.ts +32 -0
- package/src/jsonTypeResolution/resolvableTypes.ts +37 -0
- package/tsconfig.tsbuildinfo +1 -1
@@ -1,9 +1,11 @@
|
|
1
|
-
import { JsonValidationService } from './JsonValidationService';
|
1
|
+
import { ComponentInputMetaSchema, JSONSchemaService, JsonValidationService } from './JsonValidationService';
|
2
2
|
import { SchemaValidationError } from './errors/SchemaValidationError';
|
3
3
|
|
4
4
|
import validManifest from './manifest/v1/__test__/schemas/validComponent.json';
|
5
5
|
import validManifestJson from './manifest/v1/__test__/schemas/validComponentJson.json';
|
6
6
|
import { FormattedText } from './formatted-text/v1/formattedText';
|
7
|
+
import { JSONSchema } from 'json-schema-library';
|
8
|
+
import { PrimitiveType, ResolvableType, TypeResolver } from './jsonTypeResolution/arbitraryTypeResolution';
|
7
9
|
|
8
10
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
9
11
|
function expectToThrowErrorMatchingTypeAndMessage(received: Function, errorType: Function, message: string) {
|
@@ -656,3 +658,280 @@ describe('JsonValidationService', () => {
|
|
656
658
|
});
|
657
659
|
});
|
658
660
|
});
|
661
|
+
|
662
|
+
const defaultSchema: JSONSchema = {
|
663
|
+
type: 'object',
|
664
|
+
properties: {
|
665
|
+
myProperty: {
|
666
|
+
type: 'string',
|
667
|
+
},
|
668
|
+
},
|
669
|
+
required: ['myProperty'],
|
670
|
+
};
|
671
|
+
function primitiveTypeFixture<T extends string>(title: T, schema: JSONSchema = defaultSchema) {
|
672
|
+
return PrimitiveType({
|
673
|
+
...schema,
|
674
|
+
title,
|
675
|
+
});
|
676
|
+
}
|
677
|
+
|
678
|
+
function resolvableTypeFixture<T extends string>(title: T, schema: JSONSchema = defaultSchema) {
|
679
|
+
return ResolvableType({
|
680
|
+
...schema,
|
681
|
+
title,
|
682
|
+
});
|
683
|
+
}
|
684
|
+
|
685
|
+
describe('JsonSchemaService', () => {
|
686
|
+
describe('validateInput', () => {
|
687
|
+
it.each([String('123'), Number(123), [123]])(
|
688
|
+
'should validate any primitive type with its resolvable type %s',
|
689
|
+
(propertyValue) => {
|
690
|
+
const primitiveSchema = primitiveTypeFixture('MyPrimitive', { type: 'string' });
|
691
|
+
const jsonSchemaService = new JSONSchemaService(
|
692
|
+
new TypeResolver(
|
693
|
+
{
|
694
|
+
MyPrimitive: primitiveSchema,
|
695
|
+
},
|
696
|
+
{
|
697
|
+
MyResolvableNumber: resolvableTypeFixture('MyResolvableNumber', { type: 'number' }),
|
698
|
+
MyResolvableArray: resolvableTypeFixture('MyResolvableArray', { type: 'array' }),
|
699
|
+
},
|
700
|
+
{
|
701
|
+
MyPrimitive: {
|
702
|
+
MyResolvableNumber: (value: number) => value.toString(),
|
703
|
+
MyResolvableArray: (value: any[]) => value.join(''),
|
704
|
+
},
|
705
|
+
},
|
706
|
+
),
|
707
|
+
ComponentInputMetaSchema,
|
708
|
+
);
|
709
|
+
|
710
|
+
expect(
|
711
|
+
jsonSchemaService.validateInput(
|
712
|
+
{ myProperty: propertyValue },
|
713
|
+
{ type: 'object', properties: { myProperty: { type: 'MyPrimitive' } } },
|
714
|
+
),
|
715
|
+
).toEqual(true);
|
716
|
+
},
|
717
|
+
);
|
718
|
+
|
719
|
+
it('should error when a primitive type is provided a value that cannot be resolved by its resolvable types', () => {
|
720
|
+
const primitiveSchema = primitiveTypeFixture('MyPrimitive', { type: 'string' });
|
721
|
+
const jsonSchemaService = new JSONSchemaService(
|
722
|
+
new TypeResolver(
|
723
|
+
{
|
724
|
+
MyPrimitive: primitiveSchema,
|
725
|
+
},
|
726
|
+
{
|
727
|
+
MyResolvableNumber: resolvableTypeFixture('MyResolvableNumber', { type: 'number' }),
|
728
|
+
MyResolvableArray: resolvableTypeFixture('MyResolvableArray', { type: 'array' }),
|
729
|
+
},
|
730
|
+
{
|
731
|
+
MyPrimitive: {
|
732
|
+
MyResolvableNumber: (value: number) => value.toString(),
|
733
|
+
MyResolvableArray: (value: any[]) => value.join(''),
|
734
|
+
},
|
735
|
+
},
|
736
|
+
),
|
737
|
+
ComponentInputMetaSchema,
|
738
|
+
);
|
739
|
+
|
740
|
+
expect(() => {
|
741
|
+
jsonSchemaService.validateInput(
|
742
|
+
{ myProperty: true },
|
743
|
+
{ type: 'object', properties: { myProperty: { type: 'MyPrimitive' } } },
|
744
|
+
);
|
745
|
+
}).toThrowError();
|
746
|
+
});
|
747
|
+
|
748
|
+
it.each([String('123'), Number(123), [123]])(
|
749
|
+
'should validate a primitive type when defined as a ref with resolvable value %s',
|
750
|
+
(propertyValue) => {
|
751
|
+
const primitiveSchema = primitiveTypeFixture('MyPrimitive', { type: 'string' });
|
752
|
+
const jsonSchemaService = new JSONSchemaService(
|
753
|
+
new TypeResolver(
|
754
|
+
{
|
755
|
+
MyPrimitive: primitiveSchema,
|
756
|
+
},
|
757
|
+
{
|
758
|
+
MyResolvableNumber: resolvableTypeFixture('MyResolvableNumber', { type: 'number' }),
|
759
|
+
MyResolvableArray: resolvableTypeFixture('MyResolvableArray', { type: 'array' }),
|
760
|
+
},
|
761
|
+
{
|
762
|
+
MyPrimitive: {
|
763
|
+
MyResolvableNumber: (value: number) => value.toString(),
|
764
|
+
MyResolvableArray: (value: any[]) => value.join(''),
|
765
|
+
},
|
766
|
+
},
|
767
|
+
),
|
768
|
+
ComponentInputMetaSchema,
|
769
|
+
);
|
770
|
+
|
771
|
+
expect(
|
772
|
+
jsonSchemaService.validateInput(
|
773
|
+
{ myProperty: propertyValue },
|
774
|
+
{
|
775
|
+
type: 'object',
|
776
|
+
properties: { myProperty: { $ref: '#/definitions/Ref' } },
|
777
|
+
definitions: { Ref: { type: 'MyPrimitive' } },
|
778
|
+
},
|
779
|
+
),
|
780
|
+
).toEqual(true);
|
781
|
+
},
|
782
|
+
);
|
783
|
+
|
784
|
+
it('should not validate on a primitive type against a resolvable type when a resolver is not defined', () => {
|
785
|
+
const primitiveSchema = primitiveTypeFixture('MyPrimitive', { type: 'string' });
|
786
|
+
const jsonSchemaService = new JSONSchemaService(
|
787
|
+
new TypeResolver(
|
788
|
+
{
|
789
|
+
MyPrimitive: primitiveSchema,
|
790
|
+
},
|
791
|
+
{
|
792
|
+
MyResolvableNumber: resolvableTypeFixture('MyResolvableNumber', { type: 'number' }),
|
793
|
+
MyResolvableArray: resolvableTypeFixture('MyResolvableArray', { type: 'array' }),
|
794
|
+
},
|
795
|
+
{
|
796
|
+
MyPrimitive: {
|
797
|
+
MyResolvableNumber: (value: number) => value.toString(),
|
798
|
+
},
|
799
|
+
},
|
800
|
+
),
|
801
|
+
ComponentInputMetaSchema,
|
802
|
+
);
|
803
|
+
|
804
|
+
expect(() => {
|
805
|
+
jsonSchemaService.validateInput(
|
806
|
+
{ myProperty: [123] },
|
807
|
+
{ type: 'object', properties: { myProperty: { type: 'MyPrimitive' } } },
|
808
|
+
);
|
809
|
+
}).toThrowError();
|
810
|
+
});
|
811
|
+
|
812
|
+
it('should validate a primitive type against similar but different resolvable types', () => {
|
813
|
+
const primitiveSchema = primitiveTypeFixture('MyPrimitive', { type: 'string' });
|
814
|
+
const jsonSchemaService = new JSONSchemaService(
|
815
|
+
new TypeResolver(
|
816
|
+
{
|
817
|
+
MyPrimitive: primitiveSchema,
|
818
|
+
},
|
819
|
+
{
|
820
|
+
MyResolvableSrcNumber: resolvableTypeFixture('MyResolvableSrcNumber', {
|
821
|
+
type: 'object',
|
822
|
+
properties: {
|
823
|
+
src: { type: 'number' },
|
824
|
+
},
|
825
|
+
}),
|
826
|
+
MyResolvableSrcString: resolvableTypeFixture('MyResolvableSrcString', {
|
827
|
+
type: 'object',
|
828
|
+
properties: {
|
829
|
+
src: { type: 'string' },
|
830
|
+
},
|
831
|
+
}),
|
832
|
+
},
|
833
|
+
{
|
834
|
+
MyPrimitive: {
|
835
|
+
MyResolvableSrcNumber: (value: { src: number }) => value.src.toString(),
|
836
|
+
MyResolvableSrcString: (value: { src: string }) => value.src,
|
837
|
+
},
|
838
|
+
},
|
839
|
+
),
|
840
|
+
ComponentInputMetaSchema,
|
841
|
+
);
|
842
|
+
|
843
|
+
expect(
|
844
|
+
jsonSchemaService.validateInput(
|
845
|
+
{
|
846
|
+
myProperty: {
|
847
|
+
src: 123,
|
848
|
+
},
|
849
|
+
},
|
850
|
+
{ type: 'object', properties: { myProperty: { type: 'MyPrimitive' } } },
|
851
|
+
),
|
852
|
+
).toEqual(true);
|
853
|
+
expect(
|
854
|
+
jsonSchemaService.validateInput(
|
855
|
+
{
|
856
|
+
myProperty: {
|
857
|
+
src: '123',
|
858
|
+
},
|
859
|
+
},
|
860
|
+
{ type: 'object', properties: { myProperty: { type: 'MyPrimitive' } } },
|
861
|
+
),
|
862
|
+
).toEqual(true);
|
863
|
+
});
|
864
|
+
});
|
865
|
+
|
866
|
+
describe('resolveInput', () => {
|
867
|
+
it.each([String('123'), Number(123), [123]])(
|
868
|
+
'should resolve a primitive type from its resolvable type %s',
|
869
|
+
async (resolvableValue) => {
|
870
|
+
const primitiveSchema = primitiveTypeFixture('MyPrimitive', { type: 'string' });
|
871
|
+
const jsonSchemaService = new JSONSchemaService(
|
872
|
+
new TypeResolver(
|
873
|
+
{
|
874
|
+
MyPrimitive: primitiveSchema,
|
875
|
+
},
|
876
|
+
{
|
877
|
+
MyResolvableNumber: resolvableTypeFixture('MyResolvableNumber', { type: 'number' }),
|
878
|
+
MyResolvableArray: resolvableTypeFixture('MyResolvableArray', { type: 'array' }),
|
879
|
+
},
|
880
|
+
{
|
881
|
+
MyPrimitive: {
|
882
|
+
MyResolvableNumber: (value: number) => value.toString(),
|
883
|
+
MyResolvableArray: (value: any[]) => value.join(''),
|
884
|
+
},
|
885
|
+
},
|
886
|
+
),
|
887
|
+
ComponentInputMetaSchema,
|
888
|
+
);
|
889
|
+
|
890
|
+
await expect(
|
891
|
+
jsonSchemaService.resolveInput(
|
892
|
+
{ myProperty: resolvableValue },
|
893
|
+
{ type: 'object', properties: { myProperty: { type: 'MyPrimitive' } } },
|
894
|
+
),
|
895
|
+
).resolves.toEqual({ myProperty: '123' });
|
896
|
+
},
|
897
|
+
);
|
898
|
+
|
899
|
+
it.each([
|
900
|
+
[{ src: 'MyString' }, 'MyString'],
|
901
|
+
[{ src: 1132 }, '1132'],
|
902
|
+
])('should resolve a resolvable type %s against the correct resolver to %s', async (resolvableValue, output) => {
|
903
|
+
const primitiveSchema = primitiveTypeFixture('MyPrimitive', { type: 'string' });
|
904
|
+
const jsonSchemaService = new JSONSchemaService(
|
905
|
+
new TypeResolver(
|
906
|
+
{
|
907
|
+
MyPrimitive: primitiveSchema,
|
908
|
+
},
|
909
|
+
{
|
910
|
+
MyResolvableSrcString: resolvableTypeFixture('MyResolvableSrcString', {
|
911
|
+
type: 'object',
|
912
|
+
properties: { src: { type: 'string' } },
|
913
|
+
}),
|
914
|
+
MyResolvableSrcNumber: resolvableTypeFixture('MyResolvableSrcNumber', {
|
915
|
+
type: 'object',
|
916
|
+
properties: { src: { type: 'number' } },
|
917
|
+
}),
|
918
|
+
},
|
919
|
+
{
|
920
|
+
MyPrimitive: {
|
921
|
+
MyResolvableSrcNumber: (value: { src: number }) => value.src.toString(),
|
922
|
+
MyResolvableSrcString: (value: { src: string }) => value.src,
|
923
|
+
},
|
924
|
+
},
|
925
|
+
),
|
926
|
+
ComponentInputMetaSchema,
|
927
|
+
);
|
928
|
+
|
929
|
+
await expect(
|
930
|
+
jsonSchemaService.resolveInput(
|
931
|
+
{ myProperty: resolvableValue },
|
932
|
+
{ type: 'object', properties: { myProperty: { type: 'MyPrimitive' } } },
|
933
|
+
),
|
934
|
+
).resolves.toEqual({ myProperty: output });
|
935
|
+
});
|
936
|
+
});
|
937
|
+
});
|
@@ -1,3 +1,5 @@
|
|
1
|
+
import JSONQuery, { Input } from '@sagold/json-query';
|
2
|
+
|
1
3
|
import DxComponentInputSchema from './manifest/v1/DxComponentInputSchema.json';
|
2
4
|
import DxComponentIcons from './manifest/v1/DxComponentIcons.json';
|
3
5
|
import DxContentMetaSchema from './manifest/v1/DxContentMetaSchema.json';
|
@@ -7,10 +9,12 @@ import FormattedText from './formatted-text/v1/formattedText.json';
|
|
7
9
|
|
8
10
|
import v1 from './manifest/v1/v1.json';
|
9
11
|
import { SchemaValidationError } from './errors/SchemaValidationError';
|
10
|
-
import { Draft07, JSONError, JSONSchema, Draft, DraftConfig } from 'json-schema-library';
|
12
|
+
import { Draft07, JSONError, JSONSchema, Draft, DraftConfig, isJSONError } from 'json-schema-library';
|
11
13
|
|
12
14
|
import { draft07Config } from 'json-schema-library';
|
13
15
|
import { MANIFEST_MODELS } from '.';
|
16
|
+
import { AnyPrimitiveType, AnyResolvableType, TypeResolver } from './jsonTypeResolution/arbitraryTypeResolution';
|
17
|
+
import { JsonResolutionError } from './errors/JsonResolutionError';
|
14
18
|
|
15
19
|
const defaultConfig: DraftConfig = {
|
16
20
|
...draft07Config,
|
@@ -71,6 +75,7 @@ export const ComponentInputSchema = new Draft(
|
|
71
75
|
if (!resolvedSchema) {
|
72
76
|
return resolvedSchema;
|
73
77
|
}
|
78
|
+
|
74
79
|
if (resolvedSchema.type === 'FormattedText') {
|
75
80
|
return FTSchema.rootSchema;
|
76
81
|
} else if (Array.isArray(resolvedSchema.type) && resolvedSchema.type.includes('FormattedText')) {
|
@@ -117,6 +122,127 @@ v1Schema.addRemoteSchema('/DxComponentIcons.json', DxComponentIcons);
|
|
117
122
|
v1Schema.addRemoteSchema('http://json-schema.org/draft-07/schema', Draft07Schema);
|
118
123
|
v1Schema.addRemoteSchema('http://json-schema.org/draft-07/schema#', Draft07Schema);
|
119
124
|
|
125
|
+
export const ComponentInputMetaSchema: MetaSchemaInput = {
|
126
|
+
root: DxComponentInputSchema,
|
127
|
+
remotes: {
|
128
|
+
'DxComponentInputSchema.json/DxContentMetaSchema.json': DxContentMetaSchema,
|
129
|
+
},
|
130
|
+
};
|
131
|
+
|
132
|
+
export const RenderInputMetaSchema: MetaSchemaInput = {
|
133
|
+
root: Draft07Schema,
|
134
|
+
};
|
135
|
+
|
136
|
+
export const ManifestV1MetaSchema: MetaSchemaInput = {
|
137
|
+
root: v1,
|
138
|
+
remotes: {
|
139
|
+
'DxComponentInputSchema.json/DxContentMetaSchema.json': DxContentMetaSchema,
|
140
|
+
'/DxComponentInputSchema.json': DxComponentInputSchema,
|
141
|
+
'/DxComponentIcons.json': DxComponentIcons,
|
142
|
+
'http://json-schema.org/draft-07/schema': Draft07Schema,
|
143
|
+
'http://json-schema.org/draft-07/schema#': Draft07Schema,
|
144
|
+
},
|
145
|
+
};
|
146
|
+
|
147
|
+
interface MetaSchemaInput {
|
148
|
+
root: JSONSchema;
|
149
|
+
remotes?: Record<string, JSONSchema>;
|
150
|
+
}
|
151
|
+
/**
|
152
|
+
* A service that can be used to validate and resolve JSON against a schema.
|
153
|
+
*/
|
154
|
+
export class JSONSchemaService<P extends AnyPrimitiveType, R extends AnyResolvableType> {
|
155
|
+
schema: Draft;
|
156
|
+
constructor(private typeResolver: TypeResolver<P, R>, metaSchema: MetaSchemaInput) {
|
157
|
+
this.schema = new Draft(
|
158
|
+
{
|
159
|
+
...defaultConfig,
|
160
|
+
resolveRef: (schema, rootSchema) => this.doResolveRef(schema, rootSchema),
|
161
|
+
},
|
162
|
+
metaSchema.root,
|
163
|
+
);
|
164
|
+
|
165
|
+
for (const [key, value] of Object.entries(metaSchema.remotes || {})) {
|
166
|
+
this.schema.addRemoteSchema(key, value);
|
167
|
+
}
|
168
|
+
}
|
169
|
+
|
170
|
+
private doResolveRef(schema: JSONSchema, rootSchema: JSONSchema): JSONSchema {
|
171
|
+
const initialRef = draft07Config.resolveRef(schema, rootSchema);
|
172
|
+
|
173
|
+
if (!initialRef) return initialRef;
|
174
|
+
if (!this.typeResolver.isPrimitiveType(initialRef.type)) return initialRef;
|
175
|
+
|
176
|
+
return this.typeResolver.getValidationSchemaForPrimitive(initialRef.type);
|
177
|
+
}
|
178
|
+
|
179
|
+
/**
|
180
|
+
* Validate an input value against a specified schema
|
181
|
+
* @throws {SchemaValidationError} if the input is invalid
|
182
|
+
* @returns true if the input is valid
|
183
|
+
*/
|
184
|
+
public validateInput(input: unknown, inputSchema: JSONSchema = this.schema.rootSchema): true | never {
|
185
|
+
inputSchema = this.schema.compileSchema(inputSchema);
|
186
|
+
const errors = this.schema.validate(input, inputSchema);
|
187
|
+
return this.processValidationResult(errors);
|
188
|
+
}
|
189
|
+
|
190
|
+
private processValidationResult(errors: JSONError[]): true {
|
191
|
+
if (errors.length > 0) {
|
192
|
+
throw new SchemaValidationError(errors.map((a) => a.message).join(',\n'));
|
193
|
+
}
|
194
|
+
|
195
|
+
return true;
|
196
|
+
}
|
197
|
+
|
198
|
+
/**
|
199
|
+
* Resolve an input object by replacing all resolvable shapes with their resolved values
|
200
|
+
* @param input any input object which matches the input schema
|
201
|
+
* @param inputSchema a JSONSchema which provides type information about the input object
|
202
|
+
* @returns the input object with all resolvable shapes resolved
|
203
|
+
*/
|
204
|
+
public async resolveInput(input: Input, inputSchema: JSONSchema) {
|
205
|
+
const setters: Array<Promise<(input: Input) => Input>> = [];
|
206
|
+
this.schema.each(
|
207
|
+
input,
|
208
|
+
(schema, value, pointer) => {
|
209
|
+
// First we check for if value is a resolvable shape
|
210
|
+
if (!this.typeResolver.isResolvableSchema(schema)) return;
|
211
|
+
// If its a resolvable schema, it should exist in a oneOf array with other schemas
|
212
|
+
// Including a primitive schema
|
213
|
+
const allPossibleSchemas: Array<JSONSchema> = schema.oneOfSchema.oneOf;
|
214
|
+
if (isJSONError(allPossibleSchemas)) return;
|
215
|
+
|
216
|
+
const primitiveSchema = allPossibleSchemas.find((schema): schema is P =>
|
217
|
+
this.typeResolver.isPrimitiveSchema(schema),
|
218
|
+
);
|
219
|
+
if (!primitiveSchema) return;
|
220
|
+
|
221
|
+
const resolver = this.typeResolver.tryGetResolver(primitiveSchema, schema);
|
222
|
+
if (!resolver) return;
|
223
|
+
const setResolvedData = Promise.resolve()
|
224
|
+
.then(() => resolver(value))
|
225
|
+
.then((resolvedData) => (item: typeof input) => JSONQuery.set(item, pointer, resolvedData))
|
226
|
+
.catch((e) => Promise.reject(new JsonResolutionError(e, pointer, value)));
|
227
|
+
setters.push(setResolvedData);
|
228
|
+
},
|
229
|
+
inputSchema,
|
230
|
+
);
|
231
|
+
|
232
|
+
const potentialResolutionErrors = [];
|
233
|
+
for (const resolveResult of await Promise.allSettled(setters)) {
|
234
|
+
if (resolveResult.status === 'rejected') {
|
235
|
+
potentialResolutionErrors.push(resolveResult.reason);
|
236
|
+
continue;
|
237
|
+
}
|
238
|
+
|
239
|
+
input = resolveResult.value(input);
|
240
|
+
}
|
241
|
+
|
242
|
+
return input;
|
243
|
+
}
|
244
|
+
}
|
245
|
+
|
120
246
|
export class JsonValidationService {
|
121
247
|
validateManifest(manifest: unknown, version: 'v1') {
|
122
248
|
switch (version) {
|
package/src/index.ts
CHANGED
@@ -0,0 +1,134 @@
|
|
1
|
+
import { JSONSchema } from 'json-schema-library';
|
2
|
+
import { PrimitiveType, ResolvableType, TypeResolver } from './arbitraryTypeResolution';
|
3
|
+
|
4
|
+
const defaultSchema: JSONSchema = {
|
5
|
+
type: 'object',
|
6
|
+
properties: {
|
7
|
+
myProperty: {
|
8
|
+
type: 'string',
|
9
|
+
},
|
10
|
+
},
|
11
|
+
required: ['myProperty'],
|
12
|
+
};
|
13
|
+
function primitiveTypeFixture<T extends string>(title: T, schema: JSONSchema = defaultSchema) {
|
14
|
+
return PrimitiveType({
|
15
|
+
...schema,
|
16
|
+
title,
|
17
|
+
});
|
18
|
+
}
|
19
|
+
|
20
|
+
function resolvableTypeFixture<T extends string>(title: T, schema: JSONSchema = defaultSchema) {
|
21
|
+
return ResolvableType({
|
22
|
+
...schema,
|
23
|
+
title,
|
24
|
+
});
|
25
|
+
}
|
26
|
+
|
27
|
+
describe('getValidationSchemaForPrimitive', () => {
|
28
|
+
it('should return only the primitive schema when no resolvers are defined', () => {
|
29
|
+
const primitiveType = primitiveTypeFixture('MyPrimitive');
|
30
|
+
const resolvableType = resolvableTypeFixture('MyResolvable');
|
31
|
+
const resolver = new TypeResolver(
|
32
|
+
{
|
33
|
+
MyPrimitive: primitiveType,
|
34
|
+
},
|
35
|
+
{
|
36
|
+
MyResolvable: resolvableType,
|
37
|
+
},
|
38
|
+
{},
|
39
|
+
);
|
40
|
+
|
41
|
+
expect(resolver.getValidationSchemaForPrimitive('MyPrimitive')).toEqual({
|
42
|
+
oneOf: [primitiveType],
|
43
|
+
});
|
44
|
+
});
|
45
|
+
|
46
|
+
it('should return the primitive schema and the resolvable schema when a resolver is defined', () => {
|
47
|
+
const primitiveType = primitiveTypeFixture('MyPrimitive');
|
48
|
+
const resolvableType = resolvableTypeFixture('MyResolvable');
|
49
|
+
const resolver = new TypeResolver(
|
50
|
+
{
|
51
|
+
MyPrimitive: primitiveType,
|
52
|
+
},
|
53
|
+
{
|
54
|
+
MyResolvable: resolvableType,
|
55
|
+
},
|
56
|
+
{
|
57
|
+
MyPrimitive: {
|
58
|
+
MyResolvable: () => null,
|
59
|
+
},
|
60
|
+
},
|
61
|
+
);
|
62
|
+
|
63
|
+
expect(resolver.getValidationSchemaForPrimitive('MyPrimitive')).toEqual({
|
64
|
+
oneOf: [primitiveType, resolvableType],
|
65
|
+
});
|
66
|
+
});
|
67
|
+
|
68
|
+
it('should return the primitive schema and the resolvable schema when a resolver is defined for a different primitive', () => {
|
69
|
+
const primitiveType = primitiveTypeFixture('MyPrimitive');
|
70
|
+
const resolvableType = resolvableTypeFixture('MyResolvable');
|
71
|
+
const resolver = new TypeResolver(
|
72
|
+
{
|
73
|
+
MyPrimitive: primitiveType,
|
74
|
+
MyOtherPrimitive: primitiveTypeFixture('MyOtherPrimitive'),
|
75
|
+
},
|
76
|
+
{
|
77
|
+
MyResolvable: resolvableType,
|
78
|
+
},
|
79
|
+
{
|
80
|
+
MyOtherPrimitive: {
|
81
|
+
MyResolvable: () => null,
|
82
|
+
},
|
83
|
+
},
|
84
|
+
);
|
85
|
+
|
86
|
+
expect(resolver.getValidationSchemaForPrimitive('MyPrimitive')).toEqual({
|
87
|
+
oneOf: [primitiveType],
|
88
|
+
});
|
89
|
+
});
|
90
|
+
|
91
|
+
it('should error when resolver map contains a key not listed in resolver schemas', () => {
|
92
|
+
const primitiveType = primitiveTypeFixture('MyPrimitive');
|
93
|
+
const resolvableType = resolvableTypeFixture('MyResolvable');
|
94
|
+
expect(
|
95
|
+
() =>
|
96
|
+
new TypeResolver<typeof primitiveType, typeof resolvableType>(
|
97
|
+
{
|
98
|
+
MyPrimitive: primitiveType,
|
99
|
+
},
|
100
|
+
{
|
101
|
+
MyResolvable: resolvableType,
|
102
|
+
},
|
103
|
+
{
|
104
|
+
MyPrimitive: {
|
105
|
+
// @ts-expect-error - this is not a valid resolvable type
|
106
|
+
MyOtherResolvable: () => null,
|
107
|
+
},
|
108
|
+
},
|
109
|
+
),
|
110
|
+
).toThrowError();
|
111
|
+
});
|
112
|
+
|
113
|
+
it('should error when resolver map contains a key not listed in primitive schemas', () => {
|
114
|
+
const primitiveType = primitiveTypeFixture('MyPrimitive');
|
115
|
+
const resolvableType = resolvableTypeFixture('MyResolvable');
|
116
|
+
expect(
|
117
|
+
() =>
|
118
|
+
new TypeResolver<typeof primitiveType, typeof resolvableType>(
|
119
|
+
{
|
120
|
+
MyPrimitive: primitiveType,
|
121
|
+
},
|
122
|
+
{
|
123
|
+
MyResolvable: resolvableType,
|
124
|
+
},
|
125
|
+
{
|
126
|
+
// @ts-expect-error - this is not a valid primitive type
|
127
|
+
MyOtherPrimitive: {
|
128
|
+
MyResolvable: () => null,
|
129
|
+
},
|
130
|
+
},
|
131
|
+
),
|
132
|
+
).toThrowError();
|
133
|
+
});
|
134
|
+
});
|
@@ -0,0 +1,100 @@
|
|
1
|
+
import type { JSONSchema } from 'json-schema-library';
|
2
|
+
import * as t from 'ts-brand';
|
3
|
+
|
4
|
+
type MaybePromise<T> = T | Promise<T>;
|
5
|
+
|
6
|
+
type JsonResolutionSchema<TITLE extends string> = JSONSchema & { title: TITLE };
|
7
|
+
/**
|
8
|
+
* This type allows the TypeScript type to be encoded onto the JSON schema object
|
9
|
+
*/
|
10
|
+
type SchemaWithShape<TITLE extends string, SHAPE> = JsonResolutionSchema<TITLE> & { __shape__: SHAPE };
|
11
|
+
|
12
|
+
/**
|
13
|
+
* A JSON schema which represents a primitive type which can be a resolve target
|
14
|
+
*
|
15
|
+
* The brand ensures that TypeScript can differentiate between a primitive type and a resolvable type
|
16
|
+
*/
|
17
|
+
export type PrimitiveType<TITLE extends string, SHAPE> = t.Brand<SchemaWithShape<TITLE, SHAPE>, 'primitive'>;
|
18
|
+
export function PrimitiveType<SHAPE, TITLE extends string>(
|
19
|
+
jsonSchema: JsonResolutionSchema<TITLE>,
|
20
|
+
): PrimitiveType<TITLE, SHAPE> {
|
21
|
+
return jsonSchema as PrimitiveType<TITLE, SHAPE>;
|
22
|
+
}
|
23
|
+
export type AnyPrimitiveType = PrimitiveType<string, any>;
|
24
|
+
|
25
|
+
/**
|
26
|
+
* A JSON schema which represents a type which can be resolved into a primitive type
|
27
|
+
*
|
28
|
+
* The brand ensures that TypeScript can differentiate between a primitive type and a resolvable type
|
29
|
+
*/
|
30
|
+
export type ResolvableType<TITLE extends string, SHAPE> = t.Brand<SchemaWithShape<TITLE, SHAPE>, 'resolvable'>;
|
31
|
+
export function ResolvableType<SHAPE, TITLE extends string>(
|
32
|
+
jsonSchema: JsonResolutionSchema<TITLE>,
|
33
|
+
): ResolvableType<TITLE, SHAPE> {
|
34
|
+
return jsonSchema as ResolvableType<TITLE, SHAPE>;
|
35
|
+
}
|
36
|
+
export type AnyResolvableType = ResolvableType<string, any>;
|
37
|
+
|
38
|
+
type Resolver<INPUT, OUTPUT> = (input: INPUT) => MaybePromise<OUTPUT>;
|
39
|
+
|
40
|
+
/**
|
41
|
+
* A JSON Type Resolver class which stores the primitive and resolvable JSON Schema types and their resolvers
|
42
|
+
*
|
43
|
+
* No serious logic is required here. The class should only provide data access methods and type safety
|
44
|
+
*/
|
45
|
+
export class TypeResolver<P extends AnyPrimitiveType, R extends AnyResolvableType> {
|
46
|
+
constructor(
|
47
|
+
private primitives: { [K in P as P['title']]: K },
|
48
|
+
private resolvables: { [K in R as R['title']]: K },
|
49
|
+
public resolvers: {
|
50
|
+
[PT in P as PT['title']]?: {
|
51
|
+
[RT in R as RT['title']]?: Resolver<RT['__shape__'], PT['__shape__']>;
|
52
|
+
};
|
53
|
+
},
|
54
|
+
) {
|
55
|
+
for (const [primitiveKey, primitiveResolvers] of Object.entries(resolvers) as [string, Record<string, any>][]) {
|
56
|
+
if (!(primitiveKey in primitives)) {
|
57
|
+
throw new Error('Resolver keys must match a primitive schema');
|
58
|
+
}
|
59
|
+
if (!Object.keys(primitiveResolvers).every((k) => k in resolvables)) {
|
60
|
+
throw new Error('Primitive resolvers keys must match a resolvable schema');
|
61
|
+
}
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
isPrimitiveType(type: string): type is P['title'] {
|
66
|
+
return type in this.primitives;
|
67
|
+
}
|
68
|
+
|
69
|
+
isPrimitiveSchema(schema: JSONSchema): schema is P {
|
70
|
+
return this.isPrimitiveType(schema.title);
|
71
|
+
}
|
72
|
+
|
73
|
+
isResolvableSchema(schema: JSONSchema): schema is R {
|
74
|
+
return schema.title in this.resolvables;
|
75
|
+
}
|
76
|
+
|
77
|
+
getValidationSchemaForPrimitive(type: keyof typeof this.primitives) {
|
78
|
+
const primitiveSchema = this.primitives[type];
|
79
|
+
const validSchemas = [primitiveSchema, ...this.fetchResolvableSchemasForPrimitive(type)];
|
80
|
+
|
81
|
+
return {
|
82
|
+
oneOf: validSchemas,
|
83
|
+
};
|
84
|
+
}
|
85
|
+
|
86
|
+
private *fetchResolvableSchemasForPrimitive(type: keyof typeof this.primitives) {
|
87
|
+
for (const resolverKey in this.resolvers[type]) {
|
88
|
+
yield this.resolvables[resolverKey];
|
89
|
+
}
|
90
|
+
}
|
91
|
+
|
92
|
+
tryGetResolver<PS extends P, RS extends R>(
|
93
|
+
primitiveSchema: PS,
|
94
|
+
resolvableSchema: RS,
|
95
|
+
): Resolver<RS['__shape__'], PS['__shape__']> | undefined {
|
96
|
+
if (!(primitiveSchema.title in this.resolvers)) return;
|
97
|
+
// Sometimes typescript can be insanely annoying
|
98
|
+
return (this.resolvers[primitiveSchema.title as keyof typeof this.resolvers] as any)?.[resolvableSchema.title];
|
99
|
+
}
|
100
|
+
}
|