Skip to content

Commit

Permalink
feat: applyDefaults and updated playground
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Mar 23, 2021
1 parent d917543 commit 1e6550f
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 71 deletions.
114 changes: 75 additions & 39 deletions playground/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,43 +14,56 @@
<!-- Editor -->
<div class="block">
<div class="block-title">
<div class="flex select-none">
<div class="tab">
Editor
</div>
<Tabs v-model="state.editorTab" :tabs="['reference', 'input']" />
<span class="block-label">Editor</span>
</div>
<div v-if="state.editorTab === 'reference'" class="block-content">
<div class="block-info">
Reference describes object shape, defaults, and normalizer.
We can use jsdocs to set additional comments.
</div>
<Editor v-model="state.ref" language="typescript" />
</div>
<div class="block-content">
<Editor :value="state.input" language="typescript" @update:value="state.input = $event" />
<div v-if="state.editorTab === 'input'" class="block-content">
<div class="block-info">
This is user input. Using reference object, we can apply defaults.
</div>
<Editor v-model="state.input" language="typescript" />
</div>
</div>
<!-- Tabs -->
<!-- Output -->
<div class="block">
<!-- Tab buttons -->
<div class="block-title">
<div class="flex cursor-grab">
<div
v-for="tab in ['types', 'schema', 'loader']"
:key="tab"
class="tab"
:class="[tab == state.activeTab ? 'bg-gray-400' : 'bg-gray-200']"
@click="state.activeTab = tab"
>
{{ tab[0].toUpperCase() + tab.substr(1) }}
</div>
</div>
<Tabs v-model="state.outputTab" :tabs="['loader', 'schema', 'types', 'resolved']" />
<span class="block-label">Output</span>
</div>
<!-- Schema -->
<div v-if="state.activeTab === 'schema'" class="block-content">
<Editor :value="JSON.stringify(schema, null, 2)" read-only language="json" />
<div v-if="state.outputTab === 'schema'" class="block-content">
<div class="block-info">
Schema is auto generated from reference and is json-schema compliant.
</div>
<Editor :model-value="JSON.stringify(schema, null, 2)" read-only language="json" />
</div>
<!-- Types -->
<div v-if="state.activeTab === 'types'" class="block-content">
<Editor :value="types" read-only language="typescript" />
<div v-if="state.outputTab === 'types'" class="block-content">
<div class="block-info">
Types are auto generated from schema for typescript usage.
</div>
<Editor :model-value="types" read-only language="typescript" />
</div>
<!-- Loader -->
<div v-if="state.activeTab === 'loader'" class="block-content">
<Editor :value="transpiled" read-only language="typescript" />
<div v-if="state.outputTab === 'loader'" class="block-content">
<div class="block-info">
Using optional loader, we can support jsdoc to describe object.
</div>
<Editor :model-value="transpiledRef" read-only language="typescript" />
</div>
<!-- Resolved -->
<div v-if="state.outputTab === 'resolved'" class="block-content">
<div class="block-info">
We can apply reference object to user input to apply defaults.
</div>
<Editor :model-value="JSON.stringify(resolvedInput, null, 2)" read-only language="typescript" />
</div>
</div>
</main>
Expand All @@ -59,41 +72,51 @@

<script>
import 'virtual:windi.css'
import { defineComponent, defineAsyncComponent } from 'vue'
import { resolveSchema, generateDts } from '../src'
import { evaluateSource, defaultInput, persistedState, safeComputed, asyncImport } from './utils'
import LoadingComponent from './components/Loading.vue'
import { defineComponent, defineAsyncComponent, watch } from 'vue'
import { resolveSchema, generateDts, applyDefaults } from '../src'
import { evaluateSource, persistedState, safeComputed, asyncImport } from './utils'
import { defaultReference, defaultInput } from './consts'
import LoadingComponent from './components/loading.vue'
import Tabs from './components/tabs.vue'
export default defineComponent({
components: {
Tabs,
Editor: defineAsyncComponent({
loader: () => import('./components/Editor.vue'),
loader: () => import('./components/editor.vue'),
loadingComponent: LoadingComponent
})
},
setup () {
const state = persistedState({
activeTab: 'types',
editorTab: 'reference',
outputTab: 'types',
ref: defaultReference,
input: defaultInput
})
window.process = { env: {} }
const loader = asyncImport({
loader: () => import('../src/loader'),
loading: { transform: () => '{} // loader is loading...' },
error: (err) => ({ transform: () => '{} // Error loading loader: ' + err })
loading: { transform: () => 'export default {} // loader is loading...' },
error: err => ({ transform: () => 'export default {} // Error loading loader: ' + err })
})
const transpiled = safeComputed(() => loader.transform(state.input))
const parsedInput = safeComputed(() => evaluateSource(transpiled.value))
const schema = safeComputed(() => resolveSchema(parsedInput.value))
const transpiledRef = safeComputed(() => loader.transform(state.ref))
const referenceObj = safeComputed(() => evaluateSource(transpiledRef.value))
const schema = safeComputed(() => resolveSchema(referenceObj.value))
const types = safeComputed(() => generateDts(schema.value))
const inputObj = safeComputed(() => evaluateSource(state.input))
const resolvedInput = safeComputed(() => applyDefaults(referenceObj.value, inputObj.value))
return {
state,
schema,
types,
transpiled
transpiledRef,
inputObj,
resolvedInput
}
}
})
Expand All @@ -116,17 +139,30 @@ body, html, #app {
.block-title {
padding: .5em;
position: relative;
@apply border-green-500 border-b-2 flex flex-col;
}
.block-label {
position: absolute;
right: 0;
top: 0;
@apply rounded-bl-xl bg-green-400 px-3 text-white;
}
.block-content {
flex: 1;
display: flex;
flex-direction: column;
@apply: pt-3;
}
.tab {
@apply select-none px-3 mx-1 rounded;
.block-info {
padding: .25em;
@apply border-dashed border-light-blue-500 border-b-1 mb-3 text-gray-700;
}
code {
@apply bg-gray-500 rounded text-gray-100 py-.5 px-2;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ globalThis.MonacoEnvironment = {
export default defineComponent({
props: {
value: {
modelValue: {
type: String,
required: true
},
Expand All @@ -37,15 +37,15 @@ export default defineComponent({
}
},
watch: {
value (value, oldValue) {
modelValue (value, oldValue) {
if (value !== oldValue && this.readOnly) {
this.editor.setValue(value)
}
}
},
mounted () {
const editor = moncacoEditor.create(this.$refs.editor, {
value: this.value,
value: this.modelValue,
language: this.language,
readOnly: this.readOnly,
wordWrap: true,
Expand All @@ -56,7 +56,7 @@ export default defineComponent({
})
this.editor = editor
editor.onDidChangeModelContent(() => {
this.$emit('update:value', editor.getValue())
this.$emit('update:modelValue', editor.getValue())
})
// editor.onDidBlurEditorWidget(() => {
// this.$emit('update:value', editor.getValue())
Expand Down
File renamed without changes.
30 changes: 30 additions & 0 deletions playground/components/tabs.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<template>
<div class="flex cursor-grab">
<div
v-for="tab in tabs"
:key="tab"
class="select-none px-3 mx-1 rounded"
:class="[tab == modelValue ? 'bg-gray-400' : 'bg-gray-200']"
@click="$emit('update:modelValue', tab)"
>
{{ tab[0].toUpperCase() + tab.substr(1) }}
</div>
</div>
</template>

<script>
import { defineComponent } from 'vue'
export default defineComponent({
props: {
tabs: {
type: Array,
default: () => []
},
modelValue: {
type: String,
default: ''
}
}
})
</script>
29 changes: 29 additions & 0 deletions playground/consts.ts
Original file line number Diff line number Diff line change
@@ -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()
23 changes: 1 addition & 22 deletions playground/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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()
25 changes: 25 additions & 0 deletions src/defaults.ts
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { resolveSchema } from './schema'
export { generateDts } from './dts'
export { applyDefaults } from './defaults'

export * from './types'
11 changes: 7 additions & 4 deletions src/schema.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -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]
}
Expand All @@ -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
Expand Down
Loading

0 comments on commit 1e6550f

Please sign in to comment.