Generated json validator for XP content
The latest release of XP Codegen plugin comes with support for generating content validators (based on io-ts).
It will check that some json conforms to the a Content Type defined by an XML-file.
- If an object passes validation, the object will be returned from the validator
- If at least one field doesn’t pass validation, it will return an
Array
of all errors with thekey
of the failing field, and a field specificmessage
for the frontend it can look up with lib-i18n.
This has only been tested in TypeScript XP projects so far… We would love some feedback if anyone tries it with JavaScript.
The XP Codegen plugin is a Gradle plugin that generates code for your Enonic XP project. You can read more about XP Codegen plugin in this post.
Example
Lets say we have a content type defined like this in article.xml.
<?xml version="1.0" encoding="UTF-8"?>
<content-type>
<display-name>Article</display-name>
<super-type>base:structured</super-type>
<!-- We specify `codegen-output` to say we want io-ts definitions -->
<form codegen-output="IoTs">
<input name="title" type="TextLine">
<label>Title of the article</label>
<occurrences minimum="1" maximum="1"/>
</input>
<input name="body" type="HtmlArea">
<label>Main text body</label>
<occurrences minimum="0" maximum="1"/>
</input>
</form>
</content-type>
This will now generate the following TypeScript-file:
Note that it exports both a const Article
and a type Article
. These are in two different namespaces.
import * as t from 'io-ts';
export const Article = t.type({
/**
* Title of the article
*/
title: t.string,
/**
* Main text body
*/
body: t.union([t.undefined, t.string]),
});
export type Article = t.TypeOf<typeof Article>;
To use the Article
codec to validate the content we can do the following:
import {Request, Response} from 'enonic-types/controller';
import {Article} from "../../content-types/article/article";
import {Either, isRight} from "fp-ts/Either";
import {Errors} from "io-ts";
import {getErrorDetailReporter} from "enonic-wizardry/reporters/ErrorDetailReporter";
const {create} = __non_webpack_require__('/lib/xp/content');
const {run} = __non_webpack_require__('/lib/xp/context');
const {sanitize} = __non_webpack_require__('/lib/xp/common');
export function post(req: Request): Response {
const rawArticle: Partial<Article> = {
title: emptyStringToUndefined(req.params.title),
body: emptyStringToUndefined(req.params.body)
};
// `decode` is where io-ts is used to validate. It either returns:
// - Errors on the left side
// - The Article on the right side
const decoded: Either<Errors, Article> = Article.decode(rawArticle);
if(isRight(decoded)) {
const article: Article = decoded.right;
runAsSu(() => create({
displayName: article.title,
parentPath: req.params.parentPath!,
contentType: `${app.name}:article`,
name: sanitize(article.title),
data: article
})
);
return {
status: 201,
body: article
};
} else {
return {
status: 400,
/**
* If `title` was undefined, then the result of `report(decoded)` become:
* [
* {
* key: "title",
* message: "<i18n phrase with key = 'articleFormPart.error.400.title'>"
* }
* ]
*/
body: getErrorDetailReporter("articleFormPart.error").report(decoded),
};
}
}
const runContext = {
user: {
login: "su",
idProvider: "system"
},
branch: 'draft'
};
function runAsSu(f: () => void): void {
run(runContext, f);
}
function emptyStringToUndefined(str: string | undefined): string | undefined {
return (str === undefined || str === null || str.length === 0) ? undefined : str;
}