
Funksjonell programmering i Enonic XP #6 - Eksempler
Vi ser på noen eksempler på hvordan funksjonell programmering i Enonic XP kan gjøres i praksis.
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.
- enonic-fp enkapsulerer alle grensesnittene fra enonic-types, og gjør dem til rene funksjoner.
- 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
}
- Siden funksjonen vi skal returnere også heter
get()
, må vi gi den vi importerer her et nytt navn (getContent()
). - All funksjonaliteten fra
getEmployee()
i de tidligere eksemplene, er gjort i enonic-fp, medgetContent<Employee>()
hvor returtypen erIOEither<EnonicError, Content<Employee>>
. 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 enEnonicError
, som brukes til å velgestatus
iResponse
, og returneres som json innhold ibody
.ok
funksjonen returnerer enIO<Response>
hvorstatus=200
, og tar etobjekt | string
som parameter og bruker det sombody
iResponse
. I dette tilfellet vilbody
være av typeContent<Employee>
.fold()
forIOEither
returnerer (som beskrevet i forrige kapittel) enIO<Response>
, så vi må utføre sideeffektene gjennom å kalle funksjonen, og vil få returnert enResponse
som igjen kan returneres avget()
.
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
}
- 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 typeA
, og returnerer enstring
. renderer()
tar input av typeWithId<Employee>
, og bruker thymeleaf templaten "employe.html" og returnerer enIOEither<EnonicError, string>
, hvor høyresiden (som er av typestring
) er den ferdige htmlsiden.unsafeRenderErrorPage
tar en referanse til en thymeleaf side som første parameter, og enEnonicError
som andre parameter. Den templater daEnonicErroren
med templaten, og returnerer enIO<Response>
medstatus
basert påerrorKey
iEnonicError
.
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.- Vi tar den ferdige htmlsida fra punkt 4, og pakker den inn i en
status=200
iIO<Response>
. - 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 avpipe
-utrykket, for å kjøreIO
.
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
}
})
)
)();
}
- Vi gjør en
query()
for å liste ut ansatte. Det gjøres også et fulltekstsøk hviss
er satt som query parameter i http-kallet.query
-funksjonen returnerer et resultat av typenIOEither<EnonicError, QueryResponse<Employee>>.
- Vi klargjør all data som trengs for å lage en
Response
i map funksjonen. - I
fold()
-funksjonen tar vi dataen vi klargjorde imap
, og lager enIO<Response>
.ok
-funksjonen tar her en parameter nummer to som er av typePartial<Response>
, hvor vi kan sette enheader
. "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> {
...
}
- Vi har ikke implementert
validateEmployee
her, men vi ville vanligvis løst dette med io-ts biblioteket. - Vi bruker
createAndPublish<A>()
fra enonic-wizardry, som både oppretter content på draft-branchen, og publiserer til master-branchen. - 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 enPartial<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!