Skip to content

Commit e8db6ae

Browse files
committed
Add Mixed type
1 parent 707baf5 commit e8db6ae

File tree

9 files changed

+284
-4
lines changed

9 files changed

+284
-4
lines changed

src/FluentSchema.d.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { StringSchema } from './FluentSchema'
2+
13
declare namespace FluentSchema {
24
function BaseSchema<T>(opt?: SchemaOptions): T
35

@@ -105,6 +107,30 @@ declare namespace FluentSchema {
105107
propertyNames: (value: JSONSchema) => ObjectSchema
106108
}
107109

110+
/*
111+
type Mixed = ObjectSchema &
112+
StringSchema &
113+
NumberSchema &
114+
ArraySchema &
115+
IntegerSchema &
116+
BooleanSchema
117+
118+
// Define Omit. Can be defined in a utilities package
119+
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
120+
121+
type MixedSchema = Omit<ObjectSchema, "anyOf" >
122+
123+
interface MixedSchema<T> extends ObjectSchema, StringSchema, BaseSchema<T> {
124+
definition: (name: string, props?: JSONSchema) => T
125+
prop: (name: string, props?: JSONSchema) => T
126+
additionalProperties: (value: JSONSchema | boolean) => T
127+
maxProperties: (max: number) => T
128+
minProperties: (min: number) => T
129+
patternProperties: (options: PatternPropertiesOptions) => T
130+
dependencies: (options: DependenciesOptions) => T
131+
propertyNames: (value: JSONSchema) => T
132+
}*/
133+
108134
function FluentSchema(opt?: SchemaOptions): FluentSchema
109135

110136
interface SchemaOptions {
@@ -128,6 +154,7 @@ declare namespace FluentSchema {
128154
array: () => ArraySchema
129155
object: () => ObjectSchema
130156
null: () => NullSchema
157+
mixed: <T>(types: string[]) => T & any //FIXME LS it should always return T despite the method called
131158
}
132159
}
133160
export = FluentSchema

src/FluentSchema.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use strict'
2-
const { FORMATS } = require('./utils')
2+
const { FORMATS, TYPES } = require('./utils')
33

44
const { BaseSchema } = require('./BaseSchema')
55
const { NullSchema } = require('./NullSchema')
@@ -9,6 +9,7 @@ const { NumberSchema } = require('./NumberSchema')
99
const { IntegerSchema } = require('./IntegerSchema')
1010
const { ObjectSchema } = require('./ObjectSchema')
1111
const { ArraySchema } = require('./ArraySchema')
12+
const { MixedSchema } = require('./MixedSchema')
1213

1314
const initialState = {
1415
$schema: 'http://json-schema.org/draft-07/schema#',
@@ -135,10 +136,41 @@ const FluentSchema = (
135136
schema,
136137
factory: NullSchema,
137138
}).null(),
139+
140+
/**
141+
* A mixed schema is the union of multiple types (e.g. ['string', 'integer']
142+
*
143+
* @param {Array.<string>} types
144+
* @returns {MixedSchema}
145+
*/
146+
147+
mixed: types => {
148+
if (
149+
!Array.isArray(types) ||
150+
(Array.isArray(types) &&
151+
types.filter(t => !Object.values(TYPES).includes(t)).length > 0)
152+
) {
153+
throw new Error(
154+
`Invalid 'types'. It must be an array of types. Valid types are ${Object.values(
155+
TYPES
156+
).join(' | ')}`
157+
)
158+
}
159+
160+
return MixedSchema({
161+
...options,
162+
schema: {
163+
...schema,
164+
type: types,
165+
},
166+
factory: MixedSchema,
167+
})
168+
},
138169
})
139170

140171
module.exports = {
141172
FluentSchema,
142173
FORMATS,
174+
TYPES,
143175
default: FluentSchema,
144176
}

src/MixedSchema.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
'use strict'
2+
const { BaseSchema } = require('./BaseSchema')
3+
const { NullSchema } = require('./NullSchema')
4+
const { BooleanSchema } = require('./BooleanSchema')
5+
const { StringSchema } = require('./StringSchema')
6+
const { NumberSchema } = require('./NumberSchema')
7+
const { IntegerSchema } = require('./IntegerSchema')
8+
const { ObjectSchema } = require('./ObjectSchema')
9+
const { ArraySchema } = require('./ArraySchema')
10+
11+
const { TYPES, setAttribute } = require('./utils')
12+
13+
const initialState = {
14+
type: [],
15+
definitions: [],
16+
properties: [],
17+
required: [],
18+
}
19+
20+
/**
21+
* Represents a MixedSchema.
22+
* @param {Object} [options] - Options
23+
* @param {MixedSchema} [options.schema] - Default schema
24+
* @param {boolean} [options.generateIds = false] - generate the id automatically e.g. #properties.foo
25+
* @returns {StringSchema}
26+
*/
27+
28+
const MixedSchema = ({ schema = initialState, ...options } = {}) => {
29+
options = {
30+
generateIds: false,
31+
factory: MixedSchema,
32+
...options,
33+
}
34+
return {
35+
...(schema.type.includes(TYPES.STRING)
36+
? StringSchema({ ...options, schema, factory: MixedSchema })
37+
: {}),
38+
...(schema.type.includes(TYPES.NUMBER)
39+
? NumberSchema({ ...options, schema, factory: MixedSchema })
40+
: {}),
41+
...(schema.type.includes(TYPES.BOOLEAN)
42+
? BooleanSchema({ ...options, schema, factory: MixedSchema })
43+
: {}),
44+
...(schema.type.includes(TYPES.INTEGER)
45+
? IntegerSchema({ ...options, schema, factory: MixedSchema })
46+
: {}),
47+
...(schema.type.includes(TYPES.OBJECT)
48+
? ObjectSchema({ ...options, schema, factory: MixedSchema })
49+
: {}),
50+
...(schema.type.includes(TYPES.ARRAY)
51+
? ArraySchema({ ...options, schema, factory: MixedSchema })
52+
: {}),
53+
...(schema.type.includes(TYPES.NULL)
54+
? NullSchema({ ...options, schema, factory: MixedSchema })
55+
: {}),
56+
}
57+
}
58+
59+
module.exports = {
60+
MixedSchema,
61+
default: MixedSchema,
62+
}

src/MixedSchema.test.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
const { MixedSchema } = require('./MixedSchema')
2+
const { FluentSchema, FORMATS, TYPES } = require('./FluentSchema')
3+
4+
describe('MixedSchema', () => {
5+
it('defined', () => {
6+
expect(MixedSchema).toBeDefined()
7+
})
8+
9+
describe('constructor', () => {
10+
it('without params', () => {
11+
expect(MixedSchema().valueOf()).toEqual({})
12+
})
13+
})
14+
15+
describe('from FluentSchema', () => {
16+
it('valid', () => {
17+
const types = [
18+
TYPES.STRING,
19+
TYPES.NUMBER,
20+
TYPES.BOOLEAN,
21+
TYPES.INTEGER,
22+
TYPES.OBJECT,
23+
TYPES.ARRAY,
24+
TYPES.NULL,
25+
]
26+
expect(
27+
FluentSchema()
28+
.mixed(types)
29+
.valueOf()
30+
).toEqual({
31+
$schema: 'http://json-schema.org/draft-07/schema#',
32+
type: types,
33+
})
34+
})
35+
it('invalid param', () => {
36+
const types = ''
37+
expect(() => {
38+
FluentSchema().mixed(types)
39+
}).toThrow(
40+
"Invalid 'types'. It must be an array of types. Valid types are string | number | boolean | integer | object | array | null"
41+
)
42+
})
43+
44+
it('invalid type', () => {
45+
const types = ['string', 'invalid']
46+
expect(() => {
47+
FluentSchema().mixed(types)
48+
}).toThrow(
49+
"Invalid 'types'. It must be an array of types. Valid types are string | number | boolean | integer | object | array | null"
50+
)
51+
})
52+
})
53+
54+
it('sets a type object to the prop', () => {
55+
expect(
56+
FluentSchema()
57+
.object()
58+
.prop(
59+
'prop',
60+
FluentSchema()
61+
.mixed([TYPES.STRING, TYPES.NUMBER])
62+
.minimum(10)
63+
.maxLength(5)
64+
)
65+
.valueOf()
66+
).toEqual({
67+
$schema: 'http://json-schema.org/draft-07/schema#',
68+
properties: {
69+
prop: { maxLength: 5, minimum: 10, type: ['string', 'number'] },
70+
},
71+
type: 'object',
72+
})
73+
})
74+
})

src/NullSchema.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const NullSchema = ({ schema = initialState, ...options } = {}) => {
3030
* {@link reference|https://json-schema.org/latest/json-schema-validation.html#rfc.section.6.1.1}
3131
* @returns {FluentSchema}
3232
*/
33-
null: () => setAttribute({ schema, ...options }, ['type', 'null', 'any']),
33+
null: () => setAttribute({ schema, ...options }, ['type', 'null']),
3434
}
3535
}
3636

src/types/index.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use strict'
2+
// This file will be passed to the TypeScript CLI to verify our typings compile
3+
Object.defineProperty(exports, '__esModule', { value: true })
4+
const FluentSchema_1 = require('../FluentSchema')
5+
const mixed = FluentSchema_1.FluentSchema().mixed()
6+
mixed.maxLength(1).minimum(3)
7+
const schema = FluentSchema_1.FluentSchema()
8+
.object()
9+
.id('http://foo.com/user')
10+
.title('A User')
11+
.description('A User desc')
12+
.definition(
13+
'address',
14+
FluentSchema_1.FluentSchema()
15+
.object()
16+
.id('#address')
17+
.prop('country')
18+
.allOf([FluentSchema_1.FluentSchema().string()])
19+
.prop('city')
20+
.prop('zipcode')
21+
)
22+
.prop('username')
23+
.prop(
24+
'avatar',
25+
FluentSchema_1.FluentSchema()
26+
.string()
27+
.contentEncoding('base64')
28+
.contentMediaType('image/png')
29+
)
30+
.required()
31+
.prop(
32+
'password',
33+
FluentSchema_1.FluentSchema()
34+
.string()
35+
.default('123456')
36+
.minLength(6)
37+
.maxLength(12)
38+
.pattern('.*')
39+
)
40+
.required()
41+
.prop(
42+
'addresses',
43+
FluentSchema_1.FluentSchema()
44+
.array()
45+
.items([FluentSchema_1.FluentSchema().ref('#address')])
46+
)
47+
.required()
48+
.prop(
49+
'role',
50+
FluentSchema_1.FluentSchema()
51+
.object()
52+
.id('http://foo.com/role')
53+
.prop('name')
54+
.enum(['ADMIN', 'USER'])
55+
.prop('permissions')
56+
)
57+
.required()
58+
.prop('age', FluentSchema_1.FluentSchema().integer())
59+
.valueOf()
60+
console.log({ schema })

src/types/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
// This file will be passed to the TypeScript CLI to verify our typings compile
22

3-
import { FluentSchema } from '../FluentSchema'
3+
import { FluentSchema, StringSchema, NumberSchema } from '../FluentSchema'
4+
const mixed = FluentSchema().mixed<NumberSchema & StringSchema>([
5+
'string',
6+
'number',
7+
])
8+
mixed.minimum().maxLength()
9+
410
const schema = FluentSchema()
511
.object()
612
.id('http://foo.com/user')

src/types/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"compilerOptions": {
33
"target": "es6",
44
"module": "commonjs",
5-
"noEmit": true,
5+
"noEmit": false,
66
"strict": true
77
},
88
"files": ["./index.ts"]

src/utils.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,24 @@ const FORMATS = {
6868
DATE_TIME,
6969
}
7070

71+
const STRING = 'string'
72+
const NUMBER = 'number'
73+
const BOOLEAN = 'boolean'
74+
const INTEGER = 'integer'
75+
const OBJECT = 'object'
76+
const ARRAY = 'array'
77+
const NULL = 'null'
78+
79+
const TYPES = {
80+
STRING,
81+
NUMBER,
82+
BOOLEAN,
83+
INTEGER,
84+
OBJECT,
85+
ARRAY,
86+
NULL,
87+
}
88+
7189
const patchIdsWithParentId = ({ schema, generateIds, parentId }) => {
7290
const properties = Object.entries(schema.properties || {})
7391
if (properties.length === 0) return schema
@@ -162,4 +180,5 @@ module.exports = {
162180
setAttribute,
163181
setComposeType,
164182
FORMATS,
183+
TYPES,
165184
}

0 commit comments

Comments
 (0)