diff --git a/playground/App.vue b/playground/App.vue index 4ed4473..1691704 100644 --- a/playground/App.vue +++ b/playground/App.vue @@ -14,43 +14,56 @@
-
-
- Editor -
+ + Editor +
+
+
+ Reference describes object shape, defaults, and normalizer. + We can use jsdocs to set additional comments.
+
-
- +
+
+ This is user input. Using reference object, we can apply defaults. +
+
- +
-
-
-
- {{ tab[0].toUpperCase() + tab.substr(1) }} -
-
+ + Output
-
- +
+
+ Schema is auto generated from reference and is json-schema compliant. +
+
-
- +
+
+ Types are auto generated from schema for typescript usage. +
+
-
- +
+
+ Using optional loader, we can support jsdoc to describe object. +
+ +
+ +
+
+ We can apply reference object to user input to apply defaults. +
+
@@ -59,41 +72,51 @@ diff --git a/playground/consts.ts b/playground/consts.ts new file mode 100644 index 0000000..47c2ff0 --- /dev/null +++ b/playground/consts.ts @@ -0,0 +1,29 @@ +export const defaultReference = ` +export default { + name: 'default', + price: 12.5, + /** + * checked state + */ + checked: false, + dimensions: { + /** width in px */ + width: 10, + /** height in px */ + height: 10 + }, + tags: { + $resolve: (val) => ['tag1'].concat(val).filter(Boolean) + } +} +`.trim() + +export const defaultInput = ` +export default { + name: 'foo', + dimensions: { + height: 25 + }, + tags: ['custom'] +} +`.trim() diff --git a/playground/utils.ts b/playground/utils.ts index 8b8960c..0d69704 100644 --- a/playground/utils.ts +++ b/playground/utils.ts @@ -31,7 +31,7 @@ export function tryFn (fn) { export function persistedState (initialState = {}) { const state = reactive({ ...initialState, - ...tryFn(() => JSON.parse(atob(window.location.hash.substr(1)))) + ...tryFn(() => JSON.parse(atob(window.location.hash.substr(1)) || '{}')) }) watch(state, () => { window.location.hash = '#' + btoa(JSON.stringify(state)) @@ -64,24 +64,3 @@ export function asyncImport ({ loader, loading, error }) { }) return m } - -export const defaultInput = ` -export default { - name: 'vulcan', - price: 12.5, - /** - * checked state - * If is null, will use last checked status - */ - checked: false, - dimensions: { - /** width in px */ - width: 5, - /** width in px */ - height: 10 - }, - tags: { - $resolve: (val) => ['tag1'].concat(val).filter(Boolean) - } -} -`.trim() diff --git a/src/defaults.ts b/src/defaults.ts new file mode 100644 index 0000000..6be596c --- /dev/null +++ b/src/defaults.ts @@ -0,0 +1,25 @@ +import { resolveSchema } from './schema' +import type { InputObject, Schema } from './types' + +export function applyDefaults (ref: InputObject, input: InputObject) { + const schema = resolveSchema(ref, true) + return _applyDefaults(schema, input) +} + +export function _applyDefaults (schema: Schema, input: InputObject, root?: InputObject) { + if (schema.type !== 'object') { + return {} + } + const merged = {} + for (const key in schema.properties!) { + const prop = schema.properties[key] + if (prop.type === 'object') { + merged[key] = _applyDefaults(prop, input?.[key], root || input) + } else if (typeof prop.resolve === 'function') { + merged[key] = prop.resolve(input?.[key], merged, root || input) + } else { + merged[key] = input?.[key] || prop.default + } + } + return merged +} diff --git a/src/index.ts b/src/index.ts index e707455..24cb4e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export { resolveSchema } from './schema' export { generateDts } from './dts' +export { applyDefaults } from './defaults' export * from './types' diff --git a/src/schema.ts b/src/schema.ts index 6d34ba6..d107140 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1,14 +1,14 @@ import { getType, isObject, unique } from './utils' import type { InputObject, InputValue, JSValue, Schema } from './types' -export function resolveSchema (obj: InputObject) { - const schema = _resolveSchema(obj, obj) +export function resolveSchema (obj: InputObject, preserveResolve?: boolean) { + const schema = _resolveSchema(obj, obj, undefined, preserveResolve) // TODO: Create meta-schema fror superset of Schema interface // schema.$schema = 'http://json-schema.org/schema#' return schema } -function _resolveSchema (input: InputValue, parent: InputObject, root?: InputObject): Schema { +function _resolveSchema (input: InputValue, parent: InputObject, root?: InputObject, preserveResolve?: boolean): Schema { // Node is plain value if (!isObject(input)) { return normalizeSchema({ @@ -26,7 +26,7 @@ function _resolveSchema (input: InputValue, parent: InputObject, root?: InputObj const getSchema = (key) => { schema.properties = schema.properties || {} if (!schema.properties[key]) { - schema.properties[key] = _resolveSchema(node[key], proxifiedNode, root || proxifiedNode) + schema.properties[key] = _resolveSchema(node[key], proxifiedNode, root || proxifiedNode, preserveResolve) } return schema.properties[key] } @@ -46,6 +46,9 @@ function _resolveSchema (input: InputValue, parent: InputObject, root?: InputObj } if (typeof node.$resolve === 'function') { schema.default = node.$resolve(schema.default, parent, root || proxifiedNode) + if (preserveResolve) { + schema.resolve = node.$resolve + } } // Infer type from default value diff --git a/src/types.ts b/src/types.ts index 25384b3..abbe949 100644 --- a/src/types.ts +++ b/src/types.ts @@ -20,11 +20,13 @@ export type JSType = 'any' | 'array' -// A subset of JSONSchema7 +export type ResolveFn = ((value: any, parent: InputObject, root: InputObject) => JSValue) + export interface Schema { type?: JSType | JSType[] items?: Schema default?: JSValue + resolve?: ResolveFn properties?: { [key: string]: Schema } title?: string description?: string @@ -34,7 +36,7 @@ export interface Schema { export interface InputObject { [key: string]: any $schema?: Schema - $resolve?: JSValue | ((value: any, parent: InputObject, root: InputObject) => JSValue) + $resolve?: ResolveFn } export type InputValue = InputObject | JSValue