Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
523859b
Replace specs for expressions with shared library
bkeepers Mar 30, 2023
10fa1b4
Move expressions schemas into this repo
bkeepers Mar 30, 2023
a72ae35
Make Ruby 2.6 happy
bkeepers Mar 30, 2023
87c3de4
Use vitest to run tests
bkeepers Mar 31, 2023
0d065b5
Shuffle around somethings in schema.json and rename defs
bkeepers Mar 31, 2023
ee46fab
Add Min/Max functions
bkeepers Mar 31, 2023
6a2e622
Rename url-unfriendly defs
bkeepers Mar 31, 2023
08b03c1
Move expression examples out of test dir
bkeepers Mar 31, 2023
5004b6f
Initial docs for implementing functions
bkeepers Mar 31, 2023
4c35f3e
Run JS lint/tests
bkeepers Mar 31, 2023
c51eda3
Add not about running tests to expression docs
bkeepers Mar 31, 2023
50dc917
Fix README formatting
bkeepers Mar 31, 2023
6262795
Remove use of path
bkeepers Apr 1, 2023
4a10eaf
Simplfy schemas by making implementation cast function arguments to a…
bkeepers Apr 1, 2023
48308dd
Add json_schemer as a dependency
bkeepers Apr 1, 2023
81a1d65
Fix JS lint
bkeepers Apr 1, 2023
b8279c5
Validate schemas in strict mode
bkeepers Apr 2, 2023
270f290
$defs => definitions for draft 7
bkeepers Apr 3, 2023
8f69da4
Build sourcemaps
bkeepers Apr 10, 2023
a498b3a
Add explorer to inspect information about the schema
bkeepers Apr 4, 2023
a2c501a
Add simple js expression model
bkeepers Apr 4, 2023
a3b2076
Add validate method to Expression/Constant
bkeepers Apr 5, 2023
ad07179
Add simple schema class to browse and validate schemas
bkeepers Apr 8, 2023
d96ec78
Add proxying back to schema to resolve refs
bkeepers Apr 9, 2023
d134ae6
Add/Subtract/Multiply/Divide functions
bkeepers Apr 10, 2023
b4e7035
Fix bug where ajv eagerly resolves ref in array items
bkeepers Apr 10, 2023
7c5016b
use exported Schema class where possible
bkeepers Apr 10, 2023
26a08f3
Pair schema with expression and constant
bkeepers Apr 12, 2023
82cf6ac
Add operator keyword to schemas
bkeepers Apr 12, 2023
3f6d241
Allow null as a constant
bkeepers Jul 17, 2023
cfecaf5
Specify json_schemer version number
bkeepers Jul 17, 2023
e3b32c4
JS: Define expression.add(…) to build new expression
bkeepers Jul 19, 2023
0848411
Add parent to expressions, refactor
bkeepers Jul 25, 2023
639d2a7
Use external package for schemas
bkeepers Oct 12, 2023
8aa356c
Merge branch 'main' into expressions-schema
bkeepers Oct 12, 2023
6512d63
Add flipper-expressions-schema as separate gem for now
bkeepers Oct 12, 2023
21a0806
Merge remote-tracking branch 'origin/main' into expressions-schema
bkeepers Oct 12, 2023
9725f56
Ensure schemas are downloaded
bkeepers Oct 12, 2023
8e1bfb1
Ensure node is setup before bundling
bkeepers Oct 12, 2023
7fc02ff
Revert renaming of ci job
bkeepers Oct 12, 2023
15099b9
English properly
bkeepers Oct 12, 2023
6640afc
Don't auth to install npm/gems from github
bkeepers Oct 12, 2023
ed90c55
Try using the gem post_install hook to avoid issue with GitHub Actions
bkeepers Oct 13, 2023
6350f0e
Remove actions/cache, just rely on ruby/node actions for caching
bkeepers Oct 13, 2023
b36dd09
Run npm install before setting up ruby
bkeepers Oct 13, 2023
5470e59
Rename expressions repo
bkeepers Oct 13, 2023
afb896a
Merge remote-tracking branch 'origin/main' into expressions-schema
bkeepers Nov 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add proxying back to schema to resolve refs
  • Loading branch information
bkeepers committed Apr 10, 2023
commit d96ec7816b91c2ce72db046622e7d34eee0bed3e
12 changes: 6 additions & 6 deletions packages/expressions/lib/constant.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ export class Constant {
return [this.value]
}

get validator () {
return schema.get('#/definitions/constant')
get schema () {
return schema.resolve('#/definitions/constant')
}

validate (validator = this.validator) {
return schema.validate(this.value, validator)
validate (schema = this.schema) {
return schema.validate(this.value)
}

matches (localSchema) {
return this.validate(localSchema).valid
matches (schema = this.schema) {
return schema.validate(this.value).valid
}
}
10 changes: 5 additions & 5 deletions packages/expressions/lib/expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ export class Expression {
return { [this.name]: this.args.map(arg => arg.value) }
}

get validator () {
return schema.get('#')
get schema () {
return schema.resolve('#')
}

validate (validator = this.validator) {
return schema.validate(this.value, validator)
validate (schema = this.schema) {
return schema.validate(this.value)
}

matches (localSchema) {
return this.validate(localSchema).valid
return localSchema.validate(this.value).valid
}
}
88 changes: 74 additions & 14 deletions packages/expressions/lib/schemas.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,92 @@
import Ajv from 'ajv'
import addFormats from 'ajv-formats'

// Load all schemas in schemas/*.json
const modules = import.meta.glob('../schemas/*.json', { eager: true, import: 'default' })
export const schemas = Object.values(modules)
export const BaseURI = modules['../schemas/schema.json'].$id

// Create a new Ajv validator instance with all schemas loaded
const ajv = new Ajv({
schemas,
useDefaults: true,
allErrors: true,
strict: true
})
addFormats(ajv)

// Proxy to resolve $refs in schema definitions
const Dereference = {
get (target, property) {
const value = target[property]

if (Array.isArray(value)) {
// Schema definition returns an array for this property, return the array with all refs resolved
return value.map((item, i) => Schema.proxy(this.join(target.$id, `${property}/${i}`), item))
} else if (value !== null && typeof value === 'object') {
// Schema definition returns an object for this property, return the subschema and proxy it
return Schema.proxy(this.join(target.$id, property), value)
} else if (value !== undefined) {
// Schema definition returns a value for this property, just return it
return value
} else if (target.$ref) {
// Schema includes a ref, so delegate to it
return Schema.resolve(target.$ref, target.$id)[property]
}
},

join ($id, path) {
const url = new URL($id)
url.hash = [url.hash, path].join('/')
return url.toString()
}
}

// Delegate property access to the schema definition
const DelegateToDefinition = {
get (target, property) {
return target[property] ?? target.definition[property]
},

has (target, property) {
return property in target || property in target.definition
}
}

// Class to browse schemas, resolve refs, and validate data
class Schema {
constructor (schemas, baseURI) {
this.baseURI = baseURI
static resolve ($ref, $id = undefined) {
const { href } = new URL($ref, $id)
return this.proxy(href, ajv.getSchema(href).schema)
}

static proxy ($id, definition) {
return new Proxy(new Schema($id, definition), DelegateToDefinition)
}

constructor ($id, definition) {
this.definition = new Proxy({ $id, ...definition }, Dereference)
}

resolve ($ref = this.definition.$ref, $id = this.definition.$id) {
return Schema.resolve($ref, $id)
}

this.ajv = new Ajv({
schemas,
useDefaults: true,
allErrors: true,
strict: true
})
addFormats(this.ajv)
resolveAnyOf () {
return this.definition.anyOf?.map(ref => ref.resolveAnyOf())?.flat() || [this]
}

get (ref, baseURI = this.baseURI) {
return this.ajv.getSchema(new URL(ref, baseURI).href)
arrayItem (index) {
const items = this.definition.items
return Array.isArray(items) ? items[index] : items
}

validate (data, validator = this.get('#')) {
const valid = validator(data, schema)
validate (data) {
const validator = ajv.getSchema(this.definition.$id)
const valid = validator(data)
const errors = validator.errors
return { valid, errors }
}
}

export const schema = new Schema(schemas, BaseURI)
export const schema = Schema.resolve(BaseURI)
1 change: 1 addition & 0 deletions packages/expressions/schemas/Duration.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"items": [
{ "$ref": "schema.json#/definitions/number" },
{
"title": "Unit",
"anyOf": [
{
"type": "string",
Expand Down
2 changes: 2 additions & 0 deletions packages/expressions/schemas/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,15 @@
"additionalProperties": false
},
"string": {
"title": "String",
"description": "A constant string value or a function that returns a string",
"anyOf": [
{ "type": "string" },
{ "$ref": "#/definitions/function" }
]
},
"number": {
"title": "Number",
"description": "A constant numeric value or a function that returns a number",
"anyOf": [
{ "type": "number" },
Expand Down
10 changes: 5 additions & 5 deletions packages/expressions/test/constant.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { describe, test, expect } from 'vitest'
import { Constant, schema } from '../lib'

describe('Constant', () => {
describe('validator', () => {
test('returns Constant validator', () => {
expect(new Constant('string').validator.schema.title).toEqual('Constant')
describe('schema', () => {
test('returns Constant schema', () => {
expect(new Constant('string').schema.title).toEqual('Constant')
})
})

Expand All @@ -20,12 +20,12 @@ describe('Constant', () => {

describe('matches', () => {
test('returns true for matching validator', () => {
const validator = schema.get('#/definitions/constant/anyOf/0')
const validator = schema.resolve('#/definitions/constant/anyOf/0')
expect(new Constant('string').matches(validator)).toBe(true)
})

test('returns false for different schema', () => {
const validator = schema.get('#/definitions/constant/anyOf/0')
const validator = schema.resolve('#/definitions/constant/anyOf/0')
expect(new Constant(true).matches(validator)).toBe(false)
})
})
Expand Down
40 changes: 34 additions & 6 deletions packages/expressions/test/schemas.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,43 @@ describe('schema.json', () => {
})
}

describe('get', () => {
test('returns a validator', () => {
const ref = schema.get('#')
expect(ref.schema.title).toEqual('Expression')
describe('resolve', () => {
test('returns a schema', () => {
const ref = schema.resolve('#/definitions/constant')
expect(ref.title).toEqual('Constant')
expect(ref.validate(true)).toEqual({ valid: true, errors: null })
})

test('resolves refs', () => {
const ref = schema.get('#/definitions/function/properties/Any')
expect(ref.schema.title).toEqual('Any')
expect(schema.resolve('#/definitions/function/properties/Any').title).toEqual('Any')
expect(schema.definitions.function.properties.Any.title).toEqual('Any')
})
})

describe('resolveAnyOf', () => {
test('returns nested anyOf', () => {
expect(schema.resolveAnyOf()).toHaveLength(4)
})

test('returns array of schemas', () => {
const ref = schema.resolve('#/definitions/constant')
expect(ref.resolveAnyOf()).toHaveLength(3)
expect(ref.resolveAnyOf()).toEqual(ref.anyOf)
})
})

describe('arrayItem', () => {
test('returns schema for repeated array item', () => {
const any = schema.resolve("Any.schema.json")
expect(any.arrayItem(0).title).toEqual('Expression')
expect(any.arrayItem(99).title).toEqual('Expression')
})

test('returns schema for tuple', () => {
const duration = schema.resolve("Duration.schema.json")
expect(duration.arrayItem(0).title).toEqual('Number')
expect(duration.arrayItem(1).title).toEqual('Unit')
expect(duration.arrayItem(2)).toBe(undefined)
})
})
})