@squiz/dx-json-schema-lib 1.12.0-alpha.46 → 1.12.0-alpha.48
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.
- package/.npm/_logs/{2023-02-24T01_14_03_380Z-debug-0.log → 2023-02-24T06_22_01_362Z-debug-0.log} +12 -12
- 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
|
+
}
|