Skip to content

Commit d95f2ea

Browse files
committed
Add parent to expressions, refactor
- Adds `class Function extends Expression` - `class Constant extends Expresson`
1 parent b1e301f commit d95f2ea

File tree

6 files changed

+124
-106
lines changed

6 files changed

+124
-106
lines changed

lib/constant.js

Lines changed: 0 additions & 29 deletions
This file was deleted.

lib/expression.js

Lines changed: 62 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,66 @@
11
import { v4 as uuidv4 } from 'uuid'
2-
import { Constant } from './constant'
32
import { Schema } from './schemas'
43

5-
function toArray (arg) {
6-
if (Array.isArray(arg)) return arg
7-
if (arg === null) return []
8-
return [arg]
9-
}
10-
11-
// Simple model to transform this: `{ All: [{ Boolean: [true] }]`
12-
// into this: `{ id: uuidv4(), name: 'All', args: [{ id: uuidv4(), name: 'Boolean', args: [true] }] }`
134
export class Expression {
14-
static build (expression, schema = undefined) {
15-
if (expression instanceof Expression || expression instanceof Constant) {
16-
return expression
5+
static build (expression, attrs = {}) {
6+
if (expression instanceof Function || expression instanceof Constant) {
7+
return expression.clone(attrs)
178
}
189

1910
if (['number', 'string', 'boolean'].includes(typeof expression) || expression === null) {
20-
return new Constant(expression, { schema })
11+
return new Constant({ value: expression, ...attrs })
2112
} else if (typeof expression === 'object') {
2213
if (Object.keys(expression).length !== 1) {
2314
throw new TypeError(`Invalid expression: ${JSON.stringify(expression)}`)
2415
}
2516
const name = Object.keys(expression)[0]
26-
return new Expression({ name, args: expression[name] })
17+
return new Function({ name, args: expression[name] })
2718
} else {
2819
throw new TypeError(`Invalid expression: ${JSON.stringify(expression)}`)
2920
}
3021
}
3122

32-
constructor ({ name, args, id = uuidv4() }) {
23+
constructor ({ id = uuidv4(), parent = undefined }) {
3324
this.id = id
34-
this.name = name
35-
this.schema = Schema.resolve(`${name}.schema.json`)
36-
this.args = toArray(args).map((arg, i) => Expression.build(arg, this.schema.arrayItem(i)))
25+
this.parent = parent
26+
}
27+
28+
clone (attrs = {}) {
29+
return new this.constructor(Object.assign({}, this, attrs))
30+
}
31+
32+
matches (schema) {
33+
return this.validate(schema).valid
34+
}
35+
36+
add (expression) {
37+
if (this.schema.type !== 'array' || this.schema.maxItems) {
38+
return Expression.build({ All: [this, expression] })
39+
} else {
40+
return this.clone({ args: [...this.args, expression] })
41+
}
42+
}
43+
44+
get parents () {
45+
return this.parent ? [this.parent, ...this.parent.parents] : []
3746
}
3847

39-
clone ({ id = this.id, name = this.name, args = this.args } = {}) {
40-
return new Expression({ id, name, args })
48+
get depth () {
49+
return this.parents.length
50+
}
51+
}
52+
53+
// Public: A function like "All", "Any", "Equal", "Duration", etc.
54+
export class Function extends Expression {
55+
constructor ({ name, args, ...attrs }) {
56+
super(attrs)
57+
58+
this.name = name
59+
this.schema = Schema.resolve(`${name}.schema.json`)
60+
this.args = toArray(args).map((arg, i) => Expression.build(arg, {
61+
schema: this.schema.arrayItem(i),
62+
parent: this
63+
}))
4164
}
4265

4366
get value () {
@@ -47,16 +70,27 @@ export class Expression {
4770
validate (schema = this.schema) {
4871
return schema.validate(this.args.map(arg => arg.value))
4972
}
73+
}
5074

51-
matches (schema) {
52-
return this.validate(schema).valid
75+
// Public: A constant value like a "string", number (1, 3.5), or boolean (true, false).
76+
export class Constant extends Expression {
77+
constructor ({ value, schema = Schema.resolve('#'), ...attrs }) {
78+
super(attrs)
79+
this.value = value
80+
this.schema = schema
5381
}
5482

55-
add (expression) {
56-
if (this.schema.maxItems) {
57-
return Expression.build({ All: [this, expression] })
58-
} else {
59-
return this.clone({ args: [...this.args, expression] })
60-
}
83+
get args () {
84+
return [this.value]
85+
}
86+
87+
validate (schema = this.schema) {
88+
return schema.validate(this.value)
6189
}
6290
}
91+
92+
function toArray (arg) {
93+
if (Array.isArray(arg)) return arg
94+
if (arg === null) return []
95+
return [arg]
96+
}

lib/index.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
export { Schema, schemas, BaseURI } from './schemas'
2-
export { Expression } from './expression'
3-
export { Constant } from './constant'
2+
export { Expression, Function, Constant } from './expression'
43
export { default as examples } from '../examples'

test/constant.test.js

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,49 +4,49 @@ import { Constant, Schema } from '../lib'
44
describe('Constant', () => {
55
describe('schema', () => {
66
test('defaults to expression schema', () => {
7-
expect(new Constant('string').schema.title).toEqual('Expression')
7+
expect(new Constant({ value: 'string' }).schema.title).toEqual('Expression')
88
})
99

1010
test('uses provided schema', () => {
1111
const schema = Schema.resolve('#/definitions/number')
12-
const number = new Constant(42, { schema })
12+
const number = new Constant({ value: 42, schema })
1313
expect(number.schema).toEqual(schema)
14-
expect(number.clone(99).schema).toEqual(schema)
14+
expect(number.clone({ value: 99 }).schema).toEqual(schema)
1515
})
1616
})
1717

1818
describe('validate', () => {
1919
test('returns true for valid value', () => {
20-
expect(new Constant(true).validate().valid).toBe(true)
21-
expect(new Constant(false).validate().valid).toBe(true)
22-
expect(new Constant('string').validate().valid).toBe(true)
23-
expect(new Constant(42).validate().valid).toBe(true)
24-
expect(new Constant(3.14).validate().valid).toBe(true)
20+
expect(new Constant({ value: true }).validate().valid).toBe(true)
21+
expect(new Constant({ value: false }).validate().valid).toBe(true)
22+
expect(new Constant({ value: 'string' }).validate().valid).toBe(true)
23+
expect(new Constant({ value: 42 }).validate().valid).toBe(true)
24+
expect(new Constant({ value: 3.14 }).validate().valid).toBe(true)
2525
})
2626

2727
test('returns false for invalid value', () => {
28-
expect(new Constant(['array']).validate().valid).toBe(false)
28+
expect(new Constant({ value: ['array'] }).validate().valid).toBe(false)
2929
})
3030

3131
test('uses provided schema', () => {
3232
const schema = Schema.resolve('#/definitions/number')
33-
expect(new Constant(42, { schema }).validate().valid).toBe(true)
34-
expect(new Constant(42).validate(schema).valid).toBe(true)
33+
expect(new Constant({ value: 42, schema }).validate().valid).toBe(true)
34+
expect(new Constant({ value: 42 }).validate(schema).valid).toBe(true)
3535

36-
expect(new Constant('nope', { schema }).validate().valid).toBe(false)
37-
expect(new Constant('nope').validate(schema).valid).toBe(false)
36+
expect(new Constant({ value: 'nope', schema }).validate().valid).toBe(false)
37+
expect(new Constant({ value: 'nope' }).validate(schema).valid).toBe(false)
3838
})
3939
})
4040

4141
describe('matches', () => {
4242
test('returns true for matching validator', () => {
4343
const schema = Schema.resolve('#/definitions/constant/anyOf/0')
44-
expect(new Constant('string').matches(schema)).toBe(true)
44+
expect(new Constant({ value: 'string' }).matches(schema)).toBe(true)
4545
})
4646

4747
test('returns false for different schema', () => {
4848
const schema = Schema.resolve('#/definitions/constant/anyOf/0')
49-
expect(new Constant(true).matches(schema)).toBe(false)
49+
expect(new Constant({ value: true }).matches(schema)).toBe(false)
5050
})
5151
})
5252
})

test/expression.test.js

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { describe, test, expect } from 'vitest'
2-
import { Expression, Constant, Schema } from '../lib'
2+
import { Expression, Function, Constant, Schema } from '../lib'
33

44
describe('Expression', () => {
55
describe('build', () => {
66
test('builds an expression from an object', () => {
77
const expression = Expression.build({ All: [true] })
8+
expect(expression).toBeInstanceOf(Function)
89
expect(expression.name).toEqual('All')
910
expect(expression.args[0]).toBeInstanceOf(Constant)
10-
expect(expression.args[0].value).toEqual(true)
11+
expect(expression.args[0].value).toBe(true)
12+
expect(expression.args[0].parent).toBe(expression)
1113
expect(expression.value).toEqual({ All: [true] })
1214
})
1315

@@ -56,36 +58,6 @@ describe('Expression', () => {
5658
})
5759
})
5860

59-
describe('clone', () => {
60-
test('returns new expression', () => {
61-
const expression = Expression.build({ All: [true] })
62-
const clone = expression.clone()
63-
expect(clone).not.toBe(expression)
64-
expect(clone.name).toEqual(expression.name)
65-
expect(clone.args).toEqual(expression.args)
66-
expect(clone.id).toEqual(expression.id)
67-
})
68-
69-
test('builds args', () => {
70-
const expression = Expression.build({ All: [] })
71-
const clone = expression.clone({ args: [true] })
72-
expect(clone.args[0]).toBeInstanceOf(Constant)
73-
expect(clone.value).toEqual({ All: [true] })
74-
})
75-
})
76-
77-
describe('validate', () => {
78-
test('passes for valid expression', () => {
79-
const expression = Expression.build({ All: [true] })
80-
expect(expression.validate().valid).toBe(true)
81-
})
82-
83-
test('fails for invalid expression', () => {
84-
const expression = Expression.build({ Duration: [] })
85-
expect(expression.validate().valid).toBe(false)
86-
})
87-
})
88-
8961
describe('add', () => {
9062
test('Any returns new expression with added arg', () => {
9163
const expression = Expression.build({ Any: [] }).add(true)
@@ -101,5 +73,10 @@ describe('Expression', () => {
10173
const expression = Expression.build({ Equal: [1, 1] }).add(false)
10274
expect(expression.value).toEqual({ All: [{ Equal: [1, 1] }, false] })
10375
})
76+
77+
test('Constant returns new expression wrapped in All', () => {
78+
const expression = Expression.build(true).add(false)
79+
expect(expression.value).toEqual({ All: [true, false] })
80+
})
10481
})
10582
})

test/function.test.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, test, expect } from 'vitest'
2+
import { Expression, Function, Constant } from '../lib'
3+
4+
describe('Function', () => {
5+
describe('clone', () => {
6+
test('returns new expression', () => {
7+
const expression = Expression.build({ All: [true] })
8+
const clone = expression.clone()
9+
expect(clone).not.toBe(expression)
10+
expect(clone).toBeInstanceOf(Function)
11+
expect(clone.name).toEqual(expression.name)
12+
expect(clone.args).toEqual(expression.args)
13+
expect(clone.id).toEqual(expression.id)
14+
expect(clone.args[0].parent).toBe(clone)
15+
expect(clone.args[0].depth).toBe(1)
16+
})
17+
18+
test('builds args', () => {
19+
const expression = Expression.build({ All: [] })
20+
const clone = expression.clone({ args: [true] })
21+
expect(clone.args[0]).toBeInstanceOf(Constant)
22+
expect(clone.value).toEqual({ All: [true] })
23+
})
24+
})
25+
26+
describe('validate', () => {
27+
test('passes for valid expression', () => {
28+
const expression = Expression.build({ All: [true] })
29+
expect(expression.validate().valid).toBe(true)
30+
})
31+
32+
test('fails for invalid expression', () => {
33+
const expression = Expression.build({ Duration: [] })
34+
expect(expression.validate().valid).toBe(false)
35+
})
36+
})
37+
})

0 commit comments

Comments
 (0)