Skip to content

Commit

Permalink
Schema validation (#299)
Browse files Browse the repository at this point in the history
* [schema] Add a new way to traverse/prepare schema and basic validation infrastructure

* [base] Expose validation groups on result of createSchema

* [default-layout] Go into 'failure mode' and display schema errors if any

* [test-studio] Remove object without fields test case as its now a hard error

* [code-input] Fix schema warning

* [default-layout] Styling the schema error screen

* [schema] Clean up help ids

* [schema] Error message improvements + reserve a few more type names

* [schema] More help-id cleanup

* [schema] Register reference validator
  • Loading branch information
bjoerge committed Oct 25, 2017
1 parent 7c1091f commit dcc39fd
Show file tree
Hide file tree
Showing 78 changed files with 1,060 additions and 49 deletions.
8 changes: 8 additions & 0 deletions packages/@sanity/base/sanity.json
Expand Up @@ -751,6 +751,14 @@
"implements": "part:@sanity/base/time-icon",
"path": "components/icons/Time.js"
},
{
"implements": "part:@sanity/base/error-icon",
"path": "components/icons/Error.js"
},
{
"implements": "part:@sanity/base/warning-icon",
"path": "components/icons/Warning.js"
},
{
"implements": "part:@sanity/base/sanity-logo",
"path": "components/logos/SanityLogo.js"
Expand Down
3 changes: 3 additions & 0 deletions packages/@sanity/base/src/components/icons/Error.js
@@ -0,0 +1,3 @@
import MdError from 'react-icons/lib/md/error'

export default MdError
3 changes: 3 additions & 0 deletions packages/@sanity/base/src/components/icons/Warning.js
@@ -0,0 +1,3 @@
import MdWarning from 'react-icons/lib/md/warning'

export default MdWarning
27 changes: 23 additions & 4 deletions packages/@sanity/base/src/schema/createSchema.js
Expand Up @@ -3,8 +3,27 @@ import imageAsset from './types/imageAsset'
import fileAsset from './types/fileAsset'
import Schema from '@sanity/schema'
import legacyRichDate from 'part:@sanity/form-builder/input/legacy-date/schema?'
import validateSchema from '@sanity/schema/lib/sanity/validateSchema'
import groupProblems from '@sanity/schema/lib/sanity/groupProblems'

module.exports = schemaDef => Schema.compile({
name: schemaDef.name,
types: [...schemaDef.types, geopoint, legacyRichDate, imageAsset, fileAsset].filter(Boolean)
})
const isError = problem => problem.severity === 'error'

module.exports = schemaDef => {
const validated = validateSchema(schemaDef.types).getTypes()

const validation = groupProblems(validated)
const hasErrors = validation.some(group => group.problems.some(isError))

const types = hasErrors
? []
: [...schemaDef.types, geopoint, legacyRichDate, imageAsset, fileAsset].filter(Boolean)

const compiled = Schema.compile({
name: schemaDef.name,
types
})

compiled._source = schemaDef
compiled._validation = validation
return compiled
}
1 change: 1 addition & 0 deletions packages/@sanity/code-input/src/schema.js
Expand Up @@ -3,6 +3,7 @@ import Preview from './Preview'
export default {
name: 'code',
type: 'object',
title: 'Code',
fields: [
{
title: 'Code',
Expand Down
2 changes: 2 additions & 0 deletions packages/@sanity/default-layout/package.json
Expand Up @@ -36,7 +36,9 @@
"@sanity/components": "^0.117.1",
"@sanity/core": "^0.117.0",
"@sanity/form-builder": "^0.117.1",
"@sanity/generate-help-url": "^0.117.0",
"@sanity/observable": "^0.117.0",
"@sanity/schema": "^0.117.0",
"babel-plugin-lodash": "^3.2.11",
"chai": "^3.5.0",
"chai-as-promised": "^6.0.0",
Expand Down
11 changes: 10 additions & 1 deletion packages/@sanity/default-layout/src/components/DefaultLayout.js
Expand Up @@ -15,6 +15,7 @@ import Branding from './Branding'
import Ink from 'react-ink'
import HamburgerIcon from 'part:@sanity/base/hamburger-icon'
import Button from 'part:@sanity/components/buttons/default'
import {SchemaErrorReporter} from './SchemaErrorReporter'

const dataAspects = new DataAspectsResolver(schema)

Expand Down Expand Up @@ -73,7 +74,7 @@ export default withRouterHOC(class DefaultLayout extends React.Component {
})
}

render() {
renderContent = () => {
const {tools, router} = this.props
const {createMenuIsOpen, mobileMenuIsOpen} = this.state

Expand Down Expand Up @@ -139,4 +140,12 @@ export default withRouterHOC(class DefaultLayout extends React.Component {
</div>
)
}

render() {
return (
<SchemaErrorReporter>
{this.renderContent}
</SchemaErrorReporter>
)
}
})
@@ -0,0 +1,70 @@
import React from 'react'
import PropTypes from 'prop-types'
import schema from 'part:@sanity/base/schema'
import {SchemaErrors} from './SchemaErrors'

function renderPath(path) {
return path.map(segment => {
if (segment.kind === 'type') {
return `${segment.name || '<unnamed>'}(${segment.type})`
}
if (segment.kind === 'property') {
return segment.name
}
if (segment.kind === 'type') {
return `${segment.type}(${segment.name || '<unnamed>'})`
}
return null
})
.filter(Boolean)
.join(' > ')
}

function reportWarnings() {
if (!__DEV__) {
return
}
/* eslint-disable no-console */
const problemGroups = schema._validation

const groupsWithWarnings = problemGroups
.filter(group => group.problems
.some(problem => problem.severity === 'warning'))
if (groupsWithWarnings.length === 0) {
return
}
console.groupCollapsed(`⚠️ Schema has ${groupsWithWarnings.length} warnings`)
groupsWithWarnings.forEach((group, i) => {
const path = renderPath(group.path)
console.group(`%cAt ${path}`, 'color: #FF7636')
group.problems.forEach((problem, j) => {
console.log(problem.message)
})
console.groupEnd(`At ${path}`)
})
console.groupEnd('Schema warnings')
/* eslint-enable no-console */
}

export class SchemaErrorReporter extends React.Component {
componentDidMount = reportWarnings
render() {
const problemGroups = schema._validation

const groupsWithErrors = problemGroups
.filter(group => group.problems
.some(problem => problem.severity === 'error'))

if (groupsWithErrors.length > 0) {
return (
<SchemaErrors problemGroups={groupsWithErrors} />
)
}

return this.props.children()
}
}

SchemaErrorReporter.propTypes = {
children: PropTypes.func
}
86 changes: 86 additions & 0 deletions packages/@sanity/default-layout/src/components/SchemaErrors.js
@@ -0,0 +1,86 @@
import React from 'react'
import styles from './styles/SchemaErrors.css'
import ErrorIcon from 'part:@sanity/base/error-icon'
import WarningIcon from 'part:@sanity/base/warning-icon'
import generateHelpUrl from '../../../generate-help-url'

function renderPath(path) {
return path.map(segment => {
if (segment.kind === 'type') {
return (
<span className={styles.segment}>
<span key="name" className={styles.pathSegmentTypeName}>{segment.name}</span>
&ensp;<span key="type" className={styles.pathSegmentTypeType}>{segment.type}</span>
</span>
)
}
if (segment.kind === 'property') {
return (
<span className={styles.segment}>
<span className={styles.pathSegmentProperty}>{segment.name}</span>
</span>
)
}
if (segment.kind === 'type') {
return (
<span className={styles.segment}>
<span key="name" className={styles.pathSegmentTypeName}>{segment.name}</span>
<span key="type" className={styles.pathSegmentTypeType}>{segment.type}</span>
</span>
)
}
return null
})
.filter(Boolean)
}


export function SchemaErrors(props) {
const {problemGroups} = props
return (
<div className={styles.root}>
<h2 className={styles.title}>Uh oh… found errors in schema</h2>
<ul className={styles.list}>
{problemGroups.map((group, i) => {
return (
<li key={i} className={styles.listItem}>
<h2 className={styles.path}>
{renderPath(group.path)}
</h2>
<ul className={styles.problems}>
{group.problems.map((problem, j) => (
<li key={j} className={styles[`problem_${problem.severity}`]}>
<div className={styles.problemSeverity}>
<span className={styles.problemSeverityIcon}>
{problem.severity === 'error' && <ErrorIcon />}
{problem.severity === 'warning' && <WarningIcon />}
</span>
<span className={styles.problemSeverityText}>
{problem.severity}
</span>
</div>
<div className={styles.problemContent}>
<div className={styles.problemMessage}>
{problem.message}
</div>
{problem.helpId && (
<a
className={styles.problemLink}
href={generateHelpUrl(problem.helpId)}
target="_blank"
>
View documentation
</a>
)}
</div>
</li>
))}
</ul>
</li>
)
})}
</ul>
</div>

)
}
128 changes: 128 additions & 0 deletions packages/@sanity/default-layout/src/components/styles/SchemaErrors.css
@@ -0,0 +1,128 @@
@import "part:@sanity/base/theme/variables-style";

.root {
display: block;
height: 100vh;
width: 100vw;
overflow-y: auto;
color: var(--body-text);
}

.title {
margin: 0;
background-color: var(--state-danger-color);
font-size: var(--font-size-large);
padding: var(--large-padding);
color: var(--state-danger-color--text);
}

.list {
display: block;
margin: 0;
padding: 0;
padding: var(--large-padding);
}

.path {
font-size: var(--font-size-large);
word-spacing: -0.25em;
}

.segment {
@nest &:not(:last-child)::after {
padding: 0 0.5em;
content: '➝';
}
}

.problems {
display: block;
margin: 0;
padding: 0;
}

.problem {
display: flex;
margin: 0;
padding: 0;
color: var(--body-text);
font-size: var(--font-size-xsmall);
margin-bottom: var(--medium-padding);
}

.problemSeverity {
padding: 1em;
margin-right: 1em;
min-width: 4em;
text-align: center;
}

.problemSeverityIcon {
display: block;
font-size: 2em;
}

.problemSeverityText {
display: block;
font-size: var(--font-size-tiny);
margin: 1em 0;
font-weight: 700;
text-transform: uppercase;
}

.problemLink {
clear: both;
margin: 1em 0;
display: inline-block;
color: var(--body-text);

@nest &:hover {
color: var(--brand-primary);
}
}

.problemMessage {
padding-top: 0.5em;
font-family: var(--font-family-monospace);
}

.problem_error {
composes: problem;
color: var(--state-danger-color);

@nest & .problemSeverity {
color: var(--state-danger-color);
border-right: 2px solid var(--state-danger-color);
}
}

.problem_warning {
composes: problem;

@nest & .problemSeverity {
color: var(--state-warning-color);
border-right: 2px solid var(--state-warning-color);
}
}

.listItem {
display: block;
padding: 0.5em 0;
}

.arrow {
padding: 0 0.5em;
}

.pathSegmentTypeName {
font-weight: 700;
}

.pathSegmentTypeType {
font-weight: 300;
font-size: var(--font-size-small);
}

.pathSegmentProperty {
font-weight: 300;
}

0 comments on commit dcc39fd

Please sign in to comment.