Web: Zinc, Teapot, Seaside & JSON

Zinc (by Sven Van Caekenberghe) is Pharo's built-in HTTP client and server.

37.1 Zinc HTTP Components

Zinc (by Sven Van Caekenberghe) is Pharo's built-in HTTP client and server.

For routing, write a delegate object implementing handleRequest:, or use Teapot.

"Client"
ZnClient new get: 'https://pharo.org'.                   "→ the body as a String"
'https://pharo.org' asUrl retrieveContents.
ZnEasy get: 'https://pharo.org'.                         "→ a ZnResponse"

ZnClient new
    url: 'https://api.github.com/repos/pharo-project/pharo';
    accept: ZnMimeType applicationJson;
    contentReader: [ :entity | STONJSON fromString: entity contents ];
    get.                                                  "→ a Dictionary"

ZnClient new
    url: 'https://httpbin.org/post';
    entity: (ZnEntity with: '{"amount": 42}' type: ZnMimeType applicationJson);
    post;
    response.
"Server"
(ZnServer startDefaultOn: 8080)
    onRequestRespond: [ :request |
        ZnResponse ok: (ZnEntity text: 'Hello from Zinc, you asked for ' , request uri printString) ].
"Visit http://localhost:8080/anything"
ZnServer stopDefault.

37.2 Teapot: micro web framework

Teapot stopAll stops all servers. Route patterns support typed parameters (<id:IsInteger>) and regexes.

"Load: Metacello new baseline: 'Teapot'; repository: 'github://zeroflag/Teapot:master/source'; load."
| accounts |
accounts := Dictionary new.
(Teapot configure: { #port -> 8080 })
    GET: '/' -> 'Bank API';
    GET: '/hello/<name>' -> [ :req | 'Hello, ' , (req at: #name) ];
    GET: '/accounts' -> [ :req | accounts keys asArray ]; output: #json;
    GET: '/accounts/<owner>' -> [ :req |
        | acct |
        acct := accounts at: (req at: #owner).
        { #owner -> acct owner . #balance -> acct balance } asDictionary ]; output: #json;
    POST: '/accounts/<owner>' -> [ :req |
        accounts at: (req at: #owner) put: (BankAccount owner: (req at: #owner)).
        TeaResponse created ];
    exception: KeyNotFound -> (TeaResponse notFound body: 'no such account');
    start.
curl -X POST http://localhost:8080/accounts/ada
curl http://localhost:8080/accounts/ada        # {"owner":"ada","balance":0}

37.3 JSON and STON

STON (Smalltalk Object Notation) is a JSON superset that serializes arbitrary object graphs, including class names and shared references:

STON is used by Iceberg/Tonel metadata, smalltalkCI configs, and settings files.

"STONJSON (built in)"
STONJSON fromString: '{"name": "Ada", "langs": ["Smalltalk", "Lisp"]}'.   "→ a Dictionary"
STONJSON toString: { 'name' -> 'Ada' . 'age' -> 36 } asDictionary.
STONJSON toStringPretty: #(1 2 3).

"NeoJSON (load from github://svenvc/NeoJSON)"
NeoJSONReader fromString: '[1, 2, {"a": true}]'.
NeoJSONWriter toString: #(1 2 3).
NeoJSONWriter toStringPretty: (Dictionary new at: #x put: 1; yourself).

"Mapping JSON to domain objects with NeoJSON"
(NeoJSONReader on: '{"owner": "Ada", "balance": 100}' readStream)
    mapInstVarsFor: BankAccount;
    nextAs: BankAccount.                         "→ a BankAccount(Ada: 100)"
STON toString: (BankAccount owner: 'Ada' balance: 100).
"→ 'BankAccount{#owner:''Ada'',#balance:100}'"
STON fromString: 'BankAccount{#owner:''Ada'',#balance:100}'.
STON toStringPretty: (1 to: 3) asOrderedCollection.

37.4 Seaside: component-based web apps

Seaside builds web applications from stateful components with callbacks instead of URLs and templates; its control flow uses call:/answer: (implemented with continuations via thisContext).

Forms and flows:

Seaside also offers a canvas API for every HTML tag, jQuery/Ajax integration, REST support (WARestfulHandler), and runs on Pharo, Squeak, GemStone (the classic "GLASS" stack), and VA Smalltalk.

"Load: Metacello new baseline: 'Seaside3'; repository: 'github://SeasideSt/Seaside:master/repository'; load."

WAComponent << #CounterComponent
    slots: { #count };
    package: 'Seaside-Demo'

CounterComponent >> initialize
    super initialize.
    count := 0

CounterComponent >> renderContentOn: html
    html heading: count printString.
    html anchor
        callback: [ count := count + 1 ];
        with: '++'.
    html space.
    html anchor
        callback: [ count := count - 1 ];
        with: '--'

CounterComponent class >> canBeRoot
    ^ true
WAAdmin register: CounterComponent asApplicationAt: 'counter'.
ZnZincServerAdaptor startOn: 8080.
"Visit http://localhost:8080/counter"
NameEditor >> renderContentOn: html
    html form: [
        html textInput value: name; callback: [ :value | name := value ].
        html submitButton callback: [ self answer: name ]; with: 'Save' ]

"In a parent component's callback: show the editor and wait for its answer"
name := self call: NameEditor new.

37.5 Other web options

Zinc WebSockets (ZnWebSocket), Zinc REST (ZnRestServer).

Tealight, Ston-based REST frameworks; PharoJS (compile Smalltalk to JavaScript for the browser); Amber Smalltalk (Smalltalk on JavaScript).

Mustache templates, Pillar/Microdown for documentation (Pharo's own docs and books use Microdown).