Til hovedinnhold
Briller på et bord forran en laptop

Funksjonell programmering i Enonic XP #6 - Eksempler

Publisert: 13. desember 2019 av Tom Arild Jakobsen

Vi ser på noen eksempler på hvordan funksjonell programmering i Enonic XP kan gjøres i praksis.

Artikkelnummer Andre artikler i denne serien
1 Funksjonell programmering i Enonic XP #1 - Intro
2 Funksjonell programmering i Enonic XP #2 - TypeScript
3 Funksjonell programmering i Enonic XP #3 - Option
4 Funksjonell programmering i Enonic XP #4 - Either
5 Funksjonell programmering i Enonic XP #5 - IO
6 Funksjonell programmering i Enonic XP #6 - Eksempler

Endestasjonen

Etter fem kapitler har du forhåpentligvis et godt grunnlag for å forstå de fleste grunnkonseptene i funksjonell programmering. 

En av fordelene med FP som ble nevnt i kapittel 1, er at man kan skrive små gjenbrukbare funksjoner. Og her er den gode nyheten! Vi har skrevet mange av disse funksjonene for Enonic XP for deg allerede, og gjort koden open source.

I tillegg til xp-codegen-plugin og enonic-types som ble beskrevet i kapittel 2, og er agnostiske i forhold programmeringsparadigme, har vi laget to biblioteker som kun skal hjelpe med funksjonell programmering.

  1. enonic-fp enkapsulerer alle grensesnittene fra enonic-types, og gjør dem til rene funksjoner.
  2. enonic-wizardry er en samling av nyttige hjelpefunksjoner og et suplement til enonic-fp

Hvis du bruker enonic-fp og enonic-wizardry når du utvikler, ender du opp med ekstremt robust kode med god lesbarhet.

Json-service

Eksempel:

La oss ta for oss den samme kodesnutten vi har sett på i noen kapittel så langt, og reimplementere den med enonic-fp og enonic-wizardry.

import {fold} from "fp-ts/IOEither";
import {Request, Response} from "enonic-types/controller";
import {get as getContent} from "enonic-fp/content"; // 1
import {errorResponse, ok} from "enonic-fp/controller";
import {Employee} from "../../content-types/employee/employee";

// getEmployee() trengs ikke mer, siden enkapsuleringa er gjort i "enonic-fp"

export function get(req: Request): Response {
  const employee = getContent<Employee>({ key: req.params.key! }); // 2

  const program = fold(
    errorResponse({ req, i18nPrefix: 'employee.error'}), // 3
    ok // 4
  )(employee);

  return program(); // 5
}

  1. Siden funksjonen vi skal returnere også heter get(), må vi gi den vi importerer her et nytt navn (getContent()).
  2. All funksjonaliteten fra getEmployee() i de tidligere eksemplene, er gjort i enonic-fp, med getContent<Employee>() hvor returtypen er IOEither<EnonicError, Content<Employee>>.
  3. errorResponse({ req: Request, i18nPrefix: string})(err: EnonicError) tar som første parameter et i18n prefix, som brukes til å lage oppslag i i18n for feilmeldinger. Som andre parameter tar den en EnonicError, som brukes til å velge status i Response, og returneres som json innhold i body.
  4. ok funksjonen returnerer en IO<Response> hvor status=200, og tar et objekt | string som parameter og bruker det som body i Response. I dette tilfellet vil body være av type Content<Employee>.
  5. fold() for IOEither returnerer (som beskrevet i forrige kapittel) en IO<Response>, så vi må utføre sideeffektene gjennom å kalle funksjonen, og vil få returnert en Response som igjen kan returneres av get().

Bonuseksempel

Som et lite bonuseksempel vil jeg påpeke at vi kunne skrevet hele funksjonen fra forrige eksempel som en one-liner.

export function get(req: Request): Response {
  return fold(errorResponse({ req, i18nPrefix: 'employee.error'}), ok)(getContent({ key: req.params.key! }))();
}

Det er ikke det mest lesbare, men er ikke det kult?!?

Bruk med Thymeleaf

import { pipe } from "fp-ts/pipeable";
import { chain, fold, map } from "fp-ts/IOEither";
import { Request, Response } from "enonic-types/controller";
import { get as getContent } from "enonic-fp/content";
import { getRenderer } from "enonic-fp/thymeleaf";
import { ok, unsafeRenderErrorPage } from "enonic-fp/controller";
import { Employee } from "../../content-types/employee/employee";

const view = resolve('employee.html');
const errorView = resolve('error.html');
const renderer = getRenderer<Employee>(view); // 1;

export function get(req: Request): Response {
  return pipe(
    getContent({ key: req.params.key! }),
    chain(renderer), // 2
    fold(
      unsafeRenderErrorPage(errorView), // 3
      ok // 4
    )
  )(); // 5
}

  1. enonic-fp har en hjelpefunksjon som heter getRenderer<A> som tar en referanse til en thymeleaf template, og returnerer en funksjon som tar en parameter av type A, og returnerer en string.
  2. renderer() tar input av type WithId<Employee>, og bruker thymeleaf templaten "employe.html" og returnerer en IOEither<EnonicError, string>, hvor høyresiden (som er av type string) er den ferdige htmlsiden.
  3. unsafeRenderErrorPage tar en referanse til en thymeleaf side som første parameter, og en EnonicError som andre parameter. Den templater da EnonicErroren med templaten, og returnerer en IO<Response> med status basert på errorKeyEnonicError.
    Grunnen til at den har "unsafe" i navnet er at den kan kaste exceptions. Dette er ønsket adferd, for hvis den ikke klarer å rendre errorsida er det ingen grunn å gå inn i en evig loop og StackOverflow.
  4. Vi tar den ferdige htmlsida fra punkt 4, og pakker den inn i en status=200 i IO<Response>.
  5. I stedet for å trekke ut program som en egen konstant som i første eksempel, utførerer vi bare sideeffektene med det samme ved å putte () på slutten av pipe-utrykket, for å kjøre IO.

Query eksempel

import {pipe} from "fp-ts/pipeable";
import {fold, map} from "fp-ts/IOEither";
import {QueryResponse} from "enonic-types/content";
import {Request, Response} from "enonic-types/controller";
import {query} from "enonic-fp/content";
import {errorResponse, ok} from "enonic-fp/controller";
import {Employee} from "../../content-types/employee/employee";

const CONTENT_TYPE_EMPLOYEE = 'no.item.demo:employee';
const NO_SEARCH = '';

/**
 * Returns a list of employees
 */
export function get(req: Request): Response {
  return pipe(
    query<Employee>( // 1
      {
        query: (req.params.s)
          ? `fulltext("displayName^5, data.firstName, data.lastName, data.email", "${req.params.s}", "OR")`
          : NO_SEARCH,
        contentTypes: [CONTENT_TYPE_EMPLOYEE],
        start: parseInt(req.params.start!) ?? 0,
        count: parseInt(req.params.count!) ?? 1000
      }
    ),
    map( // 2
      (res: QueryResponse<Employee>) => (
        {
          employees: res.hits,
          total: res.total.toString()
        }
      )
    ),
    fold(
      errorResponse({ req, i18nPrefix: 'employee.error'}),
      ({ employees, total }) => ok(employees, { // 3
        headers: {
          'X-Total-Count': total
        }
      })
    )
  )();
}

  1. Vi gjør en query() for å liste ut ansatte. Det gjøres også et fulltekstsøk hvis s er satt som query parameter i http-kallet. query-funksjonen returnerer et resultat av typen  IOEither<EnonicError, QueryResponse<Employee>>.
  2. Vi klargjør all data som trengs for å lage en Response i map funksjonen.
  3. I fold()-funksjonen tar vi dataen vi klargjorde i map, og lager en IO<Response>. ok-funksjonen tar her en parameter nummer to som er av type Partial<Response>, hvor vi kan sette en header. "X-Total-Count" headeren er tiltenkt å brukes med paginering.

Service eksempel

I det neste eksempelet skal vi se på hvordan vi kan håndtere en POST og PUT service. Vi bruker to hjelpefunksjoner som heter createAndPublish og modifyAndPublish som både oppretter/endrer innhold på draft-branchen, og publiserer til master-branchen.

Eksempel:

import {pipe} from "fp-ts/pipeable";
import {fold, map, chain, IOEither} from "fp-ts/IOEither";
import {Request, Response} from "enonic-types/controller";
import {EnonicError} from "enonic-fp/errors";
import {errorResponse, ok, created} from "enonic-fp/controller";
import {createAndPublish, modifyAndPublish} from "enonic-wizardry/lib/content";
import {Employee} from "../../content-types/employee/employee";

const CONTENT_TYPE_EMPLOYEE = 'no.item.demo:employee';

export function post(req: Request): Response {
  return pipe(
    validateEmployee(req.params), // 1
    chain(
      (employee) => createAndPublish<Employee>( // 2
        {
          name: `${employee.firstName}-${employee.lastName}`,
          parentPath: '/a/content/path',
          contentType: CONTENT_TYPE_EMPLOYEE,
          data: employee
        }
      )
    ),
    fold(
      errorResponse(req),
      created // created betyr http-status=201
    )
  )();
}

export function put(req: Request): Response {
  return pipe(
    modifyAndPublish<Employee>(req.params._id!, req.params), // 3
    fold(
      errorResponse(req),
      ok
    )
  )();
}

function validateEmployee(data: { [key: string]: string | undefined; }): IOEither<EnonicError, Employee> {
 ...
}

  1. Vi har ikke implementert validateEmployee her, men vi ville vanligvis løst dette med io-ts biblioteket.
  2. Vi bruker createAndPublish<A>() fra enonic-wizardry, som både oppretter content på draft-branchen, og publiserer til master-branchen.
  3. Vi bruker modifyAndPublish<A>() fra enonic-wizardry, som både oppdaterer content på draft-branchen, og publiserer til master-branchen. Merk at vi ikke har noen editor callback, slik som i standard biblioteket, men tar inn en Partial<A>, som brukes for å overskrive de verdiene man ønsker.

Veien videre

Jeg håper du har synes det har vært interresant lesing, og vil ta i bruk funksjonell programmering i dine egne Enonic-prosjekter.

Det har vært fantastisk om vi klarer å bygge et funksjonel programmeringsmiljø inni Enonic-miljøet. Så hvis det er noe du lurer på eller trenger hjelp med, ta gjerne kontakt med meg. Du finner meg på "Enonic Community Slack", hvor jeg heter Tom Arild Jakobsen (Item).

Vi blir å kjøre noen interne workshops i Item om funksjonell programmering i Enonic utover våren 2020. Men om det er flere som er interresert, så må dere gjerne ta kontakt med meg, så skal vi se hva vi kan få til.

Sist men ikke minst. Det har vært veldig fint å få hjelp til å vedlikehold av alle fire prosjektene (xp-codegen-plugin, enonic-types, enonic-fp, enonic-wizardry). Så har du lyst å engasjere deg, så tar vi mer enn gjerne i mot bug reports eller pull requests! :)

God koding!