@forgehive/schema 0.1.3 → 0.2.0

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.
@@ -2,21 +2,9 @@ import { Schema } from '../index'
2
2
 
3
3
  describe('Schema Custom Number Validations', () => {
4
4
  it('should validate number min/max constraints', () => {
5
- const schema = Schema.from({
6
- age: {
7
- type: 'number',
8
- validations: {
9
- min: 18,
10
- max: 100
11
- }
12
- },
13
- score: {
14
- type: 'number',
15
- validations: {
16
- min: 0,
17
- max: 100
18
- }
19
- }
5
+ const schema = new Schema({
6
+ age: Schema.number().min(18).max(100),
7
+ score: Schema.number().min(0).max(100)
20
8
  })
21
9
 
22
10
  // Valid cases
@@ -31,37 +19,24 @@ describe('Schema Custom Number Validations', () => {
31
19
  expect(schema.validate({ age: 25, score: 101 })).toBe(false)
32
20
  })
33
21
 
34
- it('should correctly describe number validations', () => {
35
- const schema = Schema.from({
36
- age: {
37
- type: 'number',
38
- validations: {
39
- min: 18,
40
- max: 100
41
- }
42
- }
22
+ it('should describe number validations as JSON Schema', () => {
23
+ const schema = new Schema({
24
+ age: Schema.number().min(18).max(100)
43
25
  })
44
26
 
45
27
  const description = schema.describe()
46
- expect(description.age).toEqual({
28
+ expect(description.properties?.age).toEqual({
47
29
  type: 'number',
48
- validations: {
49
- min: 18,
50
- max: 100
51
- }
30
+ minimum: 18,
31
+ maximum: 100
52
32
  })
53
33
  })
54
34
  })
55
35
 
56
36
  describe('Schema Custom String Validations', () => {
57
37
  it('should validate string email format', () => {
58
- const schema = Schema.from({
59
- email: {
60
- type: 'string',
61
- validations: {
62
- email: true
63
- }
64
- }
38
+ const schema = new Schema({
39
+ email: Schema.email()
65
40
  })
66
41
 
67
42
  // Valid cases
@@ -76,14 +51,8 @@ describe('Schema Custom String Validations', () => {
76
51
  })
77
52
 
78
53
  it('should validate string length constraints', () => {
79
- const schema = Schema.from({
80
- username: {
81
- type: 'string',
82
- validations: {
83
- minLength: 3,
84
- maxLength: 20
85
- }
86
- }
54
+ const schema = new Schema({
55
+ username: Schema.string().min(3).max(20)
87
56
  })
88
57
 
89
58
  // Valid cases
@@ -97,13 +66,8 @@ describe('Schema Custom String Validations', () => {
97
66
  })
98
67
 
99
68
  it('should validate string regex pattern', () => {
100
- const schema = Schema.from({
101
- username: {
102
- type: 'string',
103
- validations: {
104
- regex: '^[a-zA-Z0-9_]+$'
105
- }
106
- }
69
+ const schema = new Schema({
70
+ username: Schema.string().regex(/^[a-zA-Z0-9_]+$/)
107
71
  })
108
72
 
109
73
  // Valid cases
@@ -117,58 +81,35 @@ describe('Schema Custom String Validations', () => {
117
81
  expect(schema.validate({ username: 'user name' })).toBe(false)
118
82
  })
119
83
 
120
- it('should correctly describe string validations', () => {
121
- const schema = Schema.from({
122
- email: {
123
- type: 'string',
124
- validations: {
125
- email: true,
126
- minLength: 5,
127
- maxLength: 100
128
- }
129
- }
84
+ it('should describe string length validations as JSON Schema', () => {
85
+ const schema = new Schema({
86
+ email: Schema.email().min(5).max(100)
130
87
  })
131
88
 
132
89
  const description = schema.describe()
133
- expect(description.email).toEqual({
90
+ expect(description.properties?.email).toMatchObject({
134
91
  type: 'string',
135
- validations: {
136
- email: true,
137
- minLength: 5,
138
- maxLength: 100
139
- }
92
+ format: 'email',
93
+ minLength: 5,
94
+ maxLength: 100
140
95
  })
141
96
  })
142
97
 
143
- it('should correctly describe string regex validation', () => {
144
- const schema = Schema.from({
145
- username: {
146
- type: 'string',
147
- validations: {
148
- regex: '^[a-zA-Z0-9_]+$'
149
- }
150
- }
98
+ it('should describe string regex validation as a pattern', () => {
99
+ const schema = new Schema({
100
+ username: Schema.string().regex(/^[a-zA-Z0-9_]+$/)
151
101
  })
152
102
 
153
103
  const description = schema.describe()
154
- expect(description.username).toEqual({
104
+ expect(description.properties?.username).toMatchObject({
155
105
  type: 'string',
156
- validations: {
157
- regex: '^[a-zA-Z0-9_]+$'
158
- }
106
+ pattern: '^[a-zA-Z0-9_]+$'
159
107
  })
160
108
  })
161
109
 
162
110
  it('should handle regex with other string validations', () => {
163
- const schema = Schema.from({
164
- username: {
165
- type: 'string',
166
- validations: {
167
- regex: '^[a-zA-Z0-9_]+$',
168
- minLength: 3,
169
- maxLength: 20
170
- }
171
- }
111
+ const schema = new Schema({
112
+ username: Schema.string().regex(/^[a-zA-Z0-9_]+$/).min(3).max(20)
172
113
  })
173
114
 
174
115
  // Valid cases
@@ -185,13 +126,8 @@ describe('Schema Custom String Validations', () => {
185
126
  })
186
127
 
187
128
  it('should handle regex patterns containing forward slashes', () => {
188
- const schema = Schema.from({
189
- path: {
190
- type: 'string',
191
- validations: {
192
- regex: '^/api/v[0-9]+/users/[0-9]+$'
193
- }
194
- }
129
+ const schema = new Schema({
130
+ path: Schema.string().regex(/^\/api\/v[0-9]+\/users\/[0-9]+$/)
195
131
  })
196
132
 
197
133
  // Valid cases
@@ -204,28 +140,20 @@ describe('Schema Custom String Validations', () => {
204
140
  expect(schema.validate({ path: '/api/v1/users' })).toBe(false)
205
141
  expect(schema.validate({ path: '/api/v1/users/abc' })).toBe(false)
206
142
 
207
- // Verify the description preserves the slashes within the pattern
143
+ // Verify the description preserves the pattern (regex source keeps escaped slashes)
208
144
  const description = schema.describe()
209
- expect(description.path).toEqual({
145
+ expect(description.properties?.path).toMatchObject({
210
146
  type: 'string',
211
- validations: {
212
- regex: '^/api/v[0-9]+/users/[0-9]+$'
213
- }
147
+ pattern: '^\\/api\\/v[0-9]+\\/users\\/[0-9]+$'
214
148
  })
215
149
  })
216
150
 
217
151
  it('should handle round trip of regex patterns containing forward slashes', () => {
218
- const schema = Schema.from({
219
- path: {
220
- type: 'string',
221
- validations: {
222
- regex: '^/api/v[0-9]+/users/[0-9]+$'
223
- }
224
- }
152
+ const schema = new Schema({
153
+ path: Schema.string().regex(/^\/api\/v[0-9]+\/users\/[0-9]+$/)
225
154
  })
226
155
 
227
156
  const description = schema.describe()
228
-
229
157
  const cloneSchema = Schema.from(description)
230
158
 
231
159
  // Valid cases
@@ -238,36 +166,21 @@ describe('Schema Custom String Validations', () => {
238
166
  expect(cloneSchema.validate({ path: '/api/v1/users' })).toBe(false)
239
167
  expect(cloneSchema.validate({ path: '/api/v1/users/abc' })).toBe(false)
240
168
 
241
- // Verify the description preserves the slashes within the pattern
169
+ // Verify the description preserves the pattern (regex source keeps escaped slashes)
242
170
  const cloneDescription = cloneSchema.describe()
243
- expect(cloneDescription).toMatchObject(description)
171
+ expect(cloneDescription.properties?.path).toMatchObject({
172
+ type: 'string',
173
+ pattern: '^\\/api\\/v[0-9]+\\/users\\/[0-9]+$'
174
+ })
244
175
  })
245
176
  })
246
177
 
247
178
  describe('Schema Roundtrip', () => {
248
179
  it('should maintain custom validations through describe/from cycle', () => {
249
- const originalSchema = Schema.from({
250
- age: {
251
- type: 'number',
252
- validations: {
253
- min: 18,
254
- max: 100
255
- }
256
- },
257
- email: {
258
- type: 'string',
259
- validations: {
260
- email: true
261
- }
262
- },
263
- username: {
264
- type: 'string',
265
- validations: {
266
- regex: '^[a-zA-Z0-9_]+$',
267
- minLength: 3,
268
- maxLength: 20
269
- }
270
- }
180
+ const originalSchema = new Schema({
181
+ age: Schema.number().min(18).max(100),
182
+ email: Schema.email(),
183
+ username: Schema.string().regex(/^[a-zA-Z0-9_]+$/).min(3).max(20)
271
184
  })
272
185
 
273
186
  const description = originalSchema.describe()
@@ -1,32 +1,42 @@
1
1
  import { Schema } from '../index'
2
2
 
3
+ // Dates are represented as ISO 8601 date-time strings (Schema.date() ->
4
+ // z.iso.datetime()), which are natively representable in JSON Schema.
5
+
3
6
  describe('Schema single date field', () => {
4
- const schema = Schema.from({
5
- createdAt: { type: 'date' }
7
+ const schema = new Schema({
8
+ createdAt: Schema.date()
6
9
  })
7
10
 
8
- it('should validate a valid date', () => {
11
+ it('should validate a valid ISO date-time string', () => {
9
12
  const data = {
10
- createdAt: new Date()
13
+ createdAt: new Date().toISOString()
11
14
  }
12
15
  expect(schema.validate(data)).toBe(true)
13
16
  })
14
17
 
15
- it('should validate a valid date string', () => {
18
+ it('should validate a fixed ISO date-time string', () => {
16
19
  const data = {
17
- createdAt: new Date('2024-03-20T12:00:00Z')
20
+ createdAt: '2024-03-20T12:00:00Z'
18
21
  }
19
22
  expect(schema.validate(data)).toBe(true)
20
23
  })
21
24
 
22
- it('should reject invalid date', () => {
25
+ it('should reject an invalid date string', () => {
23
26
  const data = {
24
27
  createdAt: 'invalid-date'
25
28
  }
26
29
  expect(schema.validate(data)).toBe(false)
27
30
  })
28
31
 
29
- it('should reject non-date value', () => {
32
+ it('should reject a Date instance (runtime type is a string now)', () => {
33
+ const data = {
34
+ createdAt: new Date()
35
+ }
36
+ expect(schema.validate(data)).toBe(false)
37
+ })
38
+
39
+ it('should reject a non-date value', () => {
30
40
  const data = {
31
41
  createdAt: 123
32
42
  }
@@ -35,32 +45,32 @@ describe('Schema single date field', () => {
35
45
  })
36
46
 
37
47
  describe('Schema array of dates', () => {
38
- const schema = Schema.from({
39
- timestamps: { type: 'array', items: { type: 'date' } }
48
+ const schema = new Schema({
49
+ timestamps: Schema.array(Schema.date())
40
50
  })
41
51
 
42
- it('should validate an array of valid dates', () => {
52
+ it('should validate an array of valid ISO date-time strings', () => {
43
53
  const data = {
44
- timestamps: [new Date(), new Date()]
54
+ timestamps: [new Date().toISOString(), new Date().toISOString()]
45
55
  }
46
56
  expect(schema.validate(data)).toBe(true)
47
57
  })
48
58
 
49
- it('should validate an array of valid date strings', () => {
59
+ it('should validate an array of fixed ISO date-time strings', () => {
50
60
  const data = {
51
- timestamps: [new Date('2024-03-20T12:00:00Z'), new Date('2024-03-21T12:00:00Z')]
61
+ timestamps: ['2024-03-20T12:00:00Z', '2024-03-21T12:00:00Z']
52
62
  }
53
63
  expect(schema.validate(data)).toBe(true)
54
64
  })
55
65
 
56
- it('should reject array with invalid date', () => {
66
+ it('should reject an array with an invalid date string', () => {
57
67
  const data = {
58
- timestamps: [new Date(), 'invalid-date']
68
+ timestamps: [new Date().toISOString(), 'invalid-date']
59
69
  }
60
70
  expect(schema.validate(data)).toBe(false)
61
71
  })
62
72
 
63
- it('should reject non-array value', () => {
73
+ it('should reject a non-array value', () => {
64
74
  const data = {
65
75
  timestamps: 'not-an-array'
66
76
  }
@@ -69,17 +79,30 @@ describe('Schema array of dates', () => {
69
79
  })
70
80
 
71
81
  describe('Dates Schema description', () => {
72
- it('should correctly describe date fields', () => {
82
+ it('should describe date fields as date-time formatted strings', () => {
73
83
  const schema = new Schema({
74
84
  createdAt: Schema.date(),
75
85
  timestamps: Schema.array(Schema.date())
76
86
  })
77
87
 
78
88
  const description = schema.describe()
79
- expect(description).toEqual({
80
- createdAt: { type: 'date' },
81
- timestamps: { type: 'array', items: { type: 'date' } }
89
+ expect(description.properties?.createdAt).toMatchObject({
90
+ type: 'string',
91
+ format: 'date-time'
92
+ })
93
+ expect(description.properties?.timestamps).toMatchObject({
94
+ type: 'array',
95
+ items: { type: 'string', format: 'date-time' }
82
96
  })
83
97
  })
84
- })
85
98
 
99
+ it('should round-trip date validation through describe/from', () => {
100
+ const schema = new Schema({
101
+ createdAt: Schema.date()
102
+ })
103
+
104
+ const clone = Schema.from(schema.describe())
105
+ expect(clone.validate({ createdAt: '2024-03-20T12:00:00Z' })).toBe(true)
106
+ expect(clone.validate({ createdAt: 'invalid-date' })).toBe(false)
107
+ })
108
+ })
@@ -0,0 +1,190 @@
1
+ import { Schema } from '../index'
2
+
3
+ // `describe()` serializes a Schema to standard JSON Schema (draft 2020-12).
4
+ // Each test builds a Schema with the Schema.* helpers and asserts the EXACT
5
+ // JSON Schema it produces, so the wire format is obvious at a glance.
6
+
7
+ const $schema = 'https://json-schema.org/draft/2020-12/schema'
8
+
9
+ describe('Schema.describe() -> JSON Schema', () => {
10
+ it('basic types', () => {
11
+ const schema = new Schema({
12
+ name: Schema.string(),
13
+ age: Schema.number(),
14
+ active: Schema.boolean(),
15
+ })
16
+
17
+ expect(schema.describe()).toEqual({
18
+ $schema,
19
+ type: 'object',
20
+ properties: {
21
+ name: { type: 'string' },
22
+ age: { type: 'number' },
23
+ active: { type: 'boolean' },
24
+ },
25
+ required: ['name', 'age', 'active'],
26
+ additionalProperties: false,
27
+ })
28
+ })
29
+
30
+ it('string validations', () => {
31
+ const schema = new Schema({
32
+ username: Schema.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/),
33
+ })
34
+
35
+ expect(schema.describe()).toEqual({
36
+ $schema,
37
+ type: 'object',
38
+ properties: {
39
+ username: {
40
+ type: 'string',
41
+ minLength: 3,
42
+ maxLength: 20,
43
+ pattern: '^[a-zA-Z0-9_]+$',
44
+ },
45
+ },
46
+ required: ['username'],
47
+ additionalProperties: false,
48
+ })
49
+ })
50
+
51
+ it('number validations', () => {
52
+ const schema = new Schema({
53
+ age: Schema.number().min(18).max(100),
54
+ })
55
+
56
+ expect(schema.describe()).toEqual({
57
+ $schema,
58
+ type: 'object',
59
+ properties: {
60
+ age: { type: 'number', minimum: 18, maximum: 100 },
61
+ },
62
+ required: ['age'],
63
+ additionalProperties: false,
64
+ })
65
+ })
66
+
67
+ it('optional fields are omitted from `required`', () => {
68
+ const schema = new Schema({
69
+ name: Schema.string(),
70
+ age: Schema.number().optional(),
71
+ })
72
+
73
+ expect(schema.describe()).toEqual({
74
+ $schema,
75
+ type: 'object',
76
+ properties: {
77
+ name: { type: 'string' },
78
+ age: { type: 'number' },
79
+ },
80
+ required: ['name'], // age is optional, so it is not required
81
+ additionalProperties: false,
82
+ })
83
+ })
84
+
85
+ it('arrays', () => {
86
+ const schema = new Schema({
87
+ tags: Schema.array(Schema.string()),
88
+ scores: Schema.array(Schema.number()),
89
+ })
90
+
91
+ expect(schema.describe()).toEqual({
92
+ $schema,
93
+ type: 'object',
94
+ properties: {
95
+ tags: { type: 'array', items: { type: 'string' } },
96
+ scores: { type: 'array', items: { type: 'number' } },
97
+ },
98
+ required: ['tags', 'scores'],
99
+ additionalProperties: false,
100
+ })
101
+ })
102
+
103
+ it('records', () => {
104
+ const schema = new Schema({
105
+ strings: Schema.stringRecord(),
106
+ mixed: Schema.mixedRecord(),
107
+ })
108
+
109
+ expect(schema.describe()).toEqual({
110
+ $schema,
111
+ type: 'object',
112
+ properties: {
113
+ strings: {
114
+ type: 'object',
115
+ propertyNames: { type: 'string' },
116
+ additionalProperties: { type: 'string' },
117
+ },
118
+ mixed: {
119
+ type: 'object',
120
+ propertyNames: { type: 'string' },
121
+ additionalProperties: {
122
+ anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }],
123
+ },
124
+ },
125
+ },
126
+ required: ['strings', 'mixed'],
127
+ additionalProperties: false,
128
+ })
129
+ })
130
+
131
+ it('nested objects', () => {
132
+ const schema = new Schema({
133
+ user: Schema.object({
134
+ name: Schema.string(),
135
+ age: Schema.number().optional(),
136
+ }),
137
+ })
138
+
139
+ expect(schema.describe()).toEqual({
140
+ $schema,
141
+ type: 'object',
142
+ properties: {
143
+ user: {
144
+ type: 'object',
145
+ properties: {
146
+ name: { type: 'string' },
147
+ age: { type: 'number' },
148
+ },
149
+ required: ['name'],
150
+ additionalProperties: false,
151
+ },
152
+ },
153
+ required: ['user'],
154
+ additionalProperties: false,
155
+ })
156
+ })
157
+
158
+ it('element descriptions', () => {
159
+ const schema = new Schema({
160
+ name: Schema.string().describe('The name of the user'),
161
+ })
162
+
163
+ expect(schema.describe()).toEqual({
164
+ $schema,
165
+ type: 'object',
166
+ properties: {
167
+ name: { type: 'string', description: 'The name of the user' },
168
+ },
169
+ required: ['name'],
170
+ additionalProperties: false,
171
+ })
172
+ })
173
+
174
+ it('string formats: email, uuid, url, date', () => {
175
+ const schema = new Schema({
176
+ email: Schema.email(),
177
+ id: Schema.uuid(),
178
+ site: Schema.url(),
179
+ when: Schema.date(),
180
+ })
181
+
182
+ // Format keywords are the stable part; the generated `pattern` strings are
183
+ // long and version-specific, so we assert the meaningful keys.
184
+ const properties = schema.describe().properties
185
+ expect(properties?.email).toMatchObject({ type: 'string', format: 'email' })
186
+ expect(properties?.id).toMatchObject({ type: 'string', format: 'uuid' })
187
+ expect(properties?.site).toMatchObject({ type: 'string', format: 'uri' })
188
+ expect(properties?.when).toMatchObject({ type: 'string', format: 'date-time' })
189
+ })
190
+ })