Generated code for server/client-side validation of content

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 the key of the failing field, and a field specific message 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;
}
1 Like