Skip to content

Commit

Permalink
Add assistant config schema api (#193)
Browse files Browse the repository at this point in the history
* Add API to get full configuration schema

Resolve #192

* Remove unused import

* Support all rule option types

Adjust `RuleOption` type to not allow null values in complex object types. This is consistent with the primitive option data types/

* Add changeset
  • Loading branch information
christianklotz authored Jun 29, 2021
1 parent 9ee7d59 commit 54a3ab9
Show file tree
Hide file tree
Showing 4 changed files with 287 additions and 1 deletion.
7 changes: 7 additions & 0 deletions .changeset/silly-mayflies-perform.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@sketch-hq/sketch-assistant-types': minor
'@sketch-hq/sketch-assistant-utils': minor
---

Add `buildAssistantConfigurationSchema` to get JSON Schema describing entire Assistant configuration
shape.
7 changes: 6 additions & 1 deletion packages/types/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,11 @@ export type AssistantConfig = {
rules: RuleConfigGroup
}

/**
* Creates the configuration JSON Schema for the given assistant definition.
*/
export type AssistantConfigSchemaCreator = (assistant: AssistantDefinition) => JSONSchema7

/**
* User-defined rule options with these names are forbidden.
*/
Expand Down Expand Up @@ -768,7 +773,7 @@ export type RuleOption =
| number
| boolean
| string[]
| { [key: string]: Maybe<string | number | boolean | string[]> }[]
| { [key: string]: string | number | boolean | string[] }[]

/**
* Async function that is expected to perform the core rule logic using the values and helper
Expand Down
223 changes: 223 additions & 0 deletions packages/utils/src/assistant-config-schema/__tests__/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { buildAssistantConfigSchema } from '..'
import { createAssistantConfig, createAssistantDefinition, createRule } from '../../test-helpers'

describe('buildAssistantConfigSchema', () => {
test('builds configuration schema', (): void => {
const assistant = createAssistantDefinition({
rules: [createRule({ name: 'foo' })],
config: createAssistantConfig({
rules: {
foo: { active: true },
},
}),
})

expect(buildAssistantConfigSchema(assistant)).toMatchInlineSnapshot(`
Object {
"properties": Object {
"rules": Object {
"properties": Object {
"foo": Object {
"properties": Object {
"active": Object {
"default": true,
"type": "boolean",
},
},
"required": Array [
"active",
],
"type": "object",
},
},
"type": "object",
},
},
"type": "object",
}
`)
})

test('builds configuration schema exclusing inactive rules', (): void => {
const assistant = createAssistantDefinition({
rules: [createRule({ name: 'foo' })],
config: createAssistantConfig({
rules: {},
}),
})

expect(buildAssistantConfigSchema(assistant)).toMatchInlineSnapshot(`
Object {
"properties": Object {
"rules": Object {
"properties": Object {},
"type": "object",
},
},
"type": "object",
}
`)
})

test('use integer option config value as default', (): void => {
const assistant = createAssistantDefinition({
rules: [
createRule({
name: 'foo',
getOptions: (helpers) => [
helpers.integerOption({
name: 'option',
title: 'integer option',
description: 'some detail',
}),
],
}),
],
config: createAssistantConfig({
rules: {
foo: { active: true, option: 1 },
},
}),
})

expect(buildAssistantConfigSchema(assistant)).toMatchInlineSnapshot(`
Object {
"properties": Object {
"rules": Object {
"properties": Object {
"foo": Object {
"properties": Object {
"active": Object {
"default": true,
"type": "boolean",
},
"option": Object {
"default": 1,
"description": "some detail",
"title": "integer option",
"type": "integer",
},
},
"required": Array [
"active",
"option",
],
"type": "object",
},
},
"type": "object",
},
},
"type": "object",
}
`)
})

test('use number option config value as default', (): void => {
const assistant = createAssistantDefinition({
rules: [
createRule({
name: 'foo',
getOptions: (helpers) => [
helpers.numberOption({
name: 'option',
title: 'number option',
description: 'some detail',
}),
],
}),
],
config: createAssistantConfig({
rules: {
foo: { active: true, option: 1.5 },
},
}),
})

expect(buildAssistantConfigSchema(assistant)).toMatchInlineSnapshot(`
Object {
"properties": Object {
"rules": Object {
"properties": Object {
"foo": Object {
"properties": Object {
"active": Object {
"default": true,
"type": "boolean",
},
"option": Object {
"default": 1.5,
"description": "some detail",
"title": "number option",
"type": "number",
},
},
"required": Array [
"active",
"option",
],
"type": "object",
},
},
"type": "object",
},
},
"type": "object",
}
`)
})

test('use string option config value as default', (): void => {
const assistant = createAssistantDefinition({
rules: [
createRule({
name: 'foo',
getOptions: (helpers) => [
helpers.stringOption({
name: 'option',
title: 'string option',
description: 'some detail',
}),
],
}),
],
config: createAssistantConfig({
rules: {
foo: { active: true, option: 'hello world' },
},
}),
})

expect(buildAssistantConfigSchema(assistant)).toMatchInlineSnapshot(`
Object {
"properties": Object {
"rules": Object {
"properties": Object {
"foo": Object {
"properties": Object {
"active": Object {
"default": true,
"type": "boolean",
},
"option": Object {
"default": "hello world",
"description": "some detail",
"title": "string option",
"type": "string",
},
},
"required": Array [
"active",
"option",
],
"type": "object",
},
},
"type": "object",
},
},
"type": "object",
}
`)
})
})
51 changes: 51 additions & 0 deletions packages/utils/src/assistant-config-schema/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
AssistantConfigSchemaCreator,
JSONSchemaProps,
ReservedRuleOptionNames,
RuleConfig,
RuleDefinition,
} from '@sketch-hq/sketch-assistant-types'
import { buildRuleOptionSchema, helpers } from '../rule-option-schemas'

const getReservedRuleOptions = (config: RuleConfig): JSONSchemaProps => {
const typeMap = {
[ReservedRuleOptionNames.active]: 'boolean',
[ReservedRuleOptionNames.severity]: 'number',
[ReservedRuleOptionNames.ruleTitle]: 'string',
}

return Object.entries(typeMap).reduce((acc, [opt, type]) => {
if (!(opt in config)) return acc
return { ...acc, [opt]: { type } }
}, {})
}

const applyRuleConfig = (ops: JSONSchemaProps, config: RuleConfig): JSONSchemaProps => {
return Object.entries(ops).reduce((acc, [key, schema]) => {
return { ...acc, [key]: { ...schema, default: config[key] } }
}, {})
}

const buildAssistantConfigSchema: AssistantConfigSchemaCreator = (assistant) => {
return {
type: 'object',
properties: {
rules: {
type: 'object',
properties: assistant.rules.reduce((acc, rule: RuleDefinition) => {
const config = assistant.config.rules[rule.name]
if (!config) return acc

const ops = [
getReservedRuleOptions(config),
...(typeof rule.getOptions !== 'undefined' ? rule.getOptions(helpers) : []),
].map((val) => applyRuleConfig(val, config))

return { ...acc, [rule.name]: buildRuleOptionSchema(ops) }
}, {}),
},
},
}
}

export { buildAssistantConfigSchema }

0 comments on commit 54a3ab9

Please sign in to comment.