Skip to content

Commit dc66179

Browse files
authored
Merge pull request #11 from jchavarri/atd
Integrate with atdgen
2 parents 856ac89 + cdc75aa commit dc66179

28 files changed

+549
-249
lines changed

.dockerignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ Dockerfile
1010
.ocamlformat
1111
docker.yml
1212
node_modules
13+
*.bs.js
14+
server/static/Index.js*
15+
/shared/gen/*.ml*
16+

.github/workflows/main.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,30 @@ jobs:
2424
--file ./Dockerfile \
2525
--build-arg BUILDKIT_INLINE_CACHE=1 \
2626
"."
27+
- name: Build client from dockerfile
28+
run: |
29+
docker build \
30+
--target client \
31+
--cache-from $CACHE_IMAGE:base \
32+
--cache-from $CACHE_IMAGE:client \
33+
--tag $CACHE_IMAGE:client \
34+
--file ./Dockerfile \
35+
--build-arg BUILDKIT_INLINE_CACHE=1 \
36+
"."
2737
- name: Build stage from dockerfile
2838
run: |
2939
docker build \
40+
--target stage \
3041
--cache-from $CACHE_IMAGE:base \
42+
--cache-from $CACHE_IMAGE:client \
3143
--cache-from $CACHE_IMAGE:stage \
3244
--tag $CACHE_IMAGE:stage \
3345
--file ./Dockerfile \
3446
--build-arg BUILDKIT_INLINE_CACHE=1 \
3547
"."
3648
- name: Push base image to docker hub
3749
run: docker push $CACHE_IMAGE:base
50+
- name: Push client image to docker hub
51+
run: docker push $CACHE_IMAGE:client
3852
- name: Push stage image to docker hub
3953
run: docker push $CACHE_IMAGE:stage

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ npm-debug.log
99
/node_modules/
1010
*.bs.js
1111
server/static/Index.js*
12+
/shared/gen/*.ml*

Dockerfile

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM ocaml/opam2:alpine-3.12-ocaml-4.10 as base
1+
FROM ocaml/opam2:debian-10-ocaml-4.10 as base
22

33
WORKDIR /ocaml_webapp
44

@@ -18,13 +18,19 @@ RUN sudo chown -R opam:nogroup . && \
1818
opam depext -ln ocaml_webapp > depexts
1919

2020
# Build client app
21-
FROM node:12 as client
21+
FROM node:12.18.3-buster as client
2222
WORKDIR /app
23-
COPY . ./
24-
RUN yarn install && yarn build && yarn webpack:production
23+
COPY package.json .
24+
COPY yarn.lock .
25+
RUN yarn install
26+
27+
COPY --from=base /home/opam/.opam/4.10/bin/atdgen /usr/local/bin/atdgen
28+
RUN chmod +x /usr/local/bin/atdgen
29+
COPY . .
30+
RUN yarn build && yarn webpack:production
2531

2632
# Create production image
27-
FROM alpine as stage
33+
FROM debian:buster-slim as stage
2834
WORKDIR /app
2935
COPY --from=base /ocaml_webapp/_build/default/server/main.exe ocaml_webapp.exe
3036
COPY --from=base /ocaml_webapp/_build/default/server/migrate/migrate.exe migrate.exe
@@ -33,6 +39,6 @@ COPY --from=client /app/server/static server/static
3339
# Don't forget to install the dependencies, noted from
3440
# the previous build.
3541
COPY --from=base /ocaml_webapp/depexts depexts
36-
RUN cat depexts | xargs apk --update add && rm -rf /var/cache/apk/*
42+
RUN apt-get update && cat depexts | xargs apt-get install -y && rm -rf /var/lib/apt/lists/*
3743

3844
CMD ./ocaml_webapp.exe --port=$PORT

bsconfig.json

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,52 @@
33
"reason": {
44
"react-jsx": 3
55
},
6+
"generators": [
7+
{
8+
"name": "atd_t",
9+
"command": "dirname=$$(dirname $in) && basename=$$(basename $in .atd) && atdgen -t $in -o $$dirname/gen/$$basename"
10+
},
11+
{
12+
"name": "atd_bs",
13+
"command": "dirname=$$(dirname $in) && basename=$$(basename $in .atd) && atdgen -bs $in -o $$dirname/gen/$$basename"
14+
}
15+
],
616
"sources": [
717
{
818
"dir": "client/src",
919
"subdirs": true
1020
},
1121
{
1222
"dir": "shared"
23+
},
24+
{
25+
"dir": "shared/gen",
26+
"generators": [
27+
{
28+
"name": "atd_t",
29+
"edge": ["excerpt_t.ml", "excerpt_t.mli", ":", "../excerpt.atd"]
30+
},
31+
{
32+
"name": "atd_bs",
33+
"edge": ["excerpt_bs.ml", "excerpt_bs.mli", ":", "../excerpt.atd"]
34+
},
35+
{
36+
"name": "atd_t",
37+
"edge": ["pageAuthorExcerpts_t.ml", "pageAuthorExcerpts_t.mli", ":", "../pageAuthorExcerpts.atd"]
38+
},
39+
{
40+
"name": "atd_bs",
41+
"edge": ["pageAuthorExcerpts_bs.ml", "pageAuthorExcerpts_bs.mli", ":", "../pageAuthorExcerpts.atd"]
42+
},
43+
{
44+
"name": "atd_t",
45+
"edge": ["pageExcerpts_t.ml", "pageExcerpts_t.mli", ":", "../pageExcerpts.atd"]
46+
},
47+
{
48+
"name": "atd_bs",
49+
"edge": ["pageExcerpts_bs.ml", "pageExcerpts_bs.mli", ":", "../pageExcerpts.atd"]
50+
}
51+
]
1352
}
1453
],
1554
"bsc-flags": ["-bs-super-errors", "-bs-no-version-header"],
@@ -21,6 +60,11 @@
2160
],
2261
"suffix": ".bs.js",
2362
"namespace": true,
24-
"bs-dependencies": ["@anuragsoni/routes", "reason-react"],
63+
"bs-dependencies": [
64+
"@ahrefs/bs-atdgen-codec-runtime",
65+
"@anuragsoni/routes",
66+
"bs-fetch",
67+
"reason-react"
68+
],
2569
"refmt": 3
2670
}

client/src/App.re

Lines changed: 79 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,93 @@
11
/** The route handlers for our app */
2-
module Get = {
3-
type request = string;
2+
module Handlers = {
3+
/** The payload available in the <script id="ocaml_webapp_page_payload" /> element */
4+
type request = option(string);
45
type response = React.element;
56

6-
/** Defines a handler that replies to requests at the root endpoint */
7-
let root = _req => <PageWelcome />;
8-
9-
/** Defines a handler that takes a path parameter from the route */
10-
let hello = (lang, _req) => <PageHello lang />;
11-
12-
/** Fallback handler in case the endpoint is called without a language parameter */
13-
let hello_fallback = _req => <PageHelloFallback />;
14-
15-
let excerpts_add = _req => <PageAddExcerpt />;
16-
// let excerpts_by_author = (name, req) =>
17-
// Lwt.(
18-
// Db.Get.excerpts_by_author(name, req)
19-
// >>= respond_or_err(Content.excerpts_listing_page)
20-
// );
21-
// let excerpts = req =>
22-
// Lwt.(
23-
// Db.Get.authors(req) >>= respond_or_err(Content.author_excerpts_page)
24-
// );
7+
module Pages = {
8+
/** Defines a handler that replies to requests at the root endpoint */
9+
let root = _req => <PageWelcome />;
10+
11+
/** Defines a handler that takes a path parameter from the route */
12+
let hello = (lang, _req) => {
13+
<PageHello lang />;
14+
};
15+
16+
/** Fallback handler in case the endpoint is called without a language parameter */
17+
let hello_fallback = _req => <PageHelloFallback />;
18+
19+
let excerpts_add = _req => <PageAddExcerpt />;
20+
21+
let excerpts_by_author = (authorName, payload) =>
22+
switch (payload) {
23+
| Some(p) =>
24+
switch (p->Js.Json.parseExn->PageExcerpts_bs.read_payload) {
25+
| excerpts => <PageExcerpts excerpts />
26+
| exception _exn =>
27+
Js.log("Couldn't parse excerpts from JSON payload " ++ p);
28+
<PageNotFound />;
29+
}
30+
| None =>
31+
<Client.FetchRender
32+
url={Routes.sprintf(
33+
Router.ApiRoutes.excerpts_by_author(),
34+
authorName,
35+
)}
36+
decoder=PageExcerpts_bs.read_payload>
37+
{(
38+
excerpts => {
39+
<PageExcerpts excerpts />;
40+
}
41+
)}
42+
</Client.FetchRender>
43+
};
44+
45+
let authors_with_excerpts = payload =>
46+
switch (payload) {
47+
| Some(p) =>
48+
switch (p->Js.Json.parseExn->PageAuthorExcerpts_bs.read_payload) {
49+
| authors => <PageAuthorExcerpts authors />
50+
| exception _exn =>
51+
Js.log("Couldn't parse authors from JSON payload " ++ p);
52+
<PageNotFound />;
53+
}
54+
| None =>
55+
<Client.FetchRender
56+
url={Routes.sprintf(Router.ApiRoutes.authors_with_excerpts())}
57+
decoder=PageAuthorExcerpts_bs.read_payload>
58+
{(authors => <PageAuthorExcerpts authors />)}
59+
</Client.FetchRender>
60+
};
61+
};
62+
module Api = {
63+
// Api routes should never be loaded from React app, show the backing page in case it happens
64+
let excerpts_by_author = Pages.excerpts_by_author;
65+
let authors_with_excerpts = Pages.authors_with_excerpts;
66+
};
2567
};
2668

27-
module Router = Router.Make(Get);
28-
let router = Routes.one_of(Router.routes);
69+
module Router = Router.Make(Handlers);
70+
let router = Method_routes.one_of(Router.routes);
71+
2972
[@react.component]
3073
let make = () => {
3174
let url = ReasonReactRouter.useUrl();
3275
let target = url.path->Array.of_list->Js.Array2.joinWith("/");
33-
// let excerpts = [
34-
// {
35-
// Excerpt_t.author: "Hey",
36-
// excerpt: "one excerpt",
37-
// source: "sdfdsf",
38-
// page: Some("2"),
39-
// },
40-
// ];
76+
let payloadElement = Bindings.getElementById(Api.payload_id);
77+
4178
switch (
42-
Routes.match'(router, ~target=Js.Global.decodeURIComponent(target))
79+
Method_routes.match'(
80+
~meth=`GET,
81+
~target=Js.Global.decodeURIComponent(target),
82+
router,
83+
)
4384
) {
4485
| None => <PageNotFound />
45-
| Some(h) => h(target)
86+
| Some(handler) =>
87+
let payload = payloadElement->Belt.Option.map(Bindings.innerHTML);
88+
/* After processing the payload from server, it can be safely removed so other pages don't try to decode it again
89+
as the client will take over navigation + data fetching through API */
90+
payloadElement->Belt.Option.forEach(e => e->Bindings.remove());
91+
handler(payload);
4692
};
4793
};

client/src/Bindings.re

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[@bs.val] [@bs.return nullable]
2+
external getElementById: string => option(Dom.element) =
3+
"document.getElementById";
4+
5+
[@bs.get] external innerHTML: Dom.element => string = "innerHTML";
6+
7+
[@bs.send] external remove: (Dom.element, unit) => unit = "remove";

client/src/Client.re

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
module Result = Belt.Result;
2+
3+
type data('a, 'b) =
4+
| Loading
5+
| Finished(Result.t('a, 'b));
6+
7+
let get = (url, decoder) => {
8+
Js.Promise.(
9+
Fetch.fetchWithInit(url, Fetch.RequestInit.make(~method_=Get, ()))
10+
|> then_(res => {
11+
res->Fetch.Response.status >= 400
12+
? Js.log(
13+
"Server returned status code "
14+
++ res->Fetch.Response.status->string_of_int
15+
++ " for url: "
16+
++ url,
17+
)
18+
: ();
19+
Fetch.Response.text(res)
20+
|> then_(jsonText =>
21+
try(jsonText->Js.Json.parseExn->decoder->Result.Ok->resolve) {
22+
| Js.Exn.Error(error) =>
23+
let errMsg = error->Js.Exn.message;
24+
`DecodingError(errMsg)->Result.Error->resolve;
25+
}
26+
);
27+
})
28+
|> catch(err => {
29+
Js.log("Network error for url: " ++ url);
30+
`NetworkError(err)->Result.Error->resolve;
31+
})
32+
);
33+
};
34+
35+
let usePrevious = value => {
36+
let valueRef = React.useRef(value);
37+
React.useEffect(() => {
38+
valueRef.current = value;
39+
None;
40+
});
41+
valueRef.current;
42+
};
43+
44+
module FetchRender = {
45+
[@react.component]
46+
let make = (~url, ~decoder, ~children) => {
47+
let (data, setData) = React.useState(() => Loading);
48+
let urlChanged = usePrevious(url) != url;
49+
let data = urlChanged ? Loading : data;
50+
51+
React.useEffect2(
52+
() => {
53+
setData(_ => Loading);
54+
get(url, decoder)
55+
|> Js.Promise.then_(res =>
56+
setData(_ => Finished(res))->Js.Promise.resolve
57+
)
58+
|> ignore;
59+
None;
60+
},
61+
(url, decoder),
62+
);
63+
switch (data) {
64+
| Loading => <div> {React.string("Loading")} </div>
65+
| Finished(Ok(payload)) => children(payload)
66+
| Finished(Error(_)) => <div> {React.string("Error")} </div>
67+
};
68+
};
69+
};

client/src/Index.re

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
[%bs.raw {|require("./index.css")|}];
22

3-
[@bs.val] [@bs.return nullable]
4-
external getElementById: string => option(Dom.element) =
5-
"document.getElementById";
6-
7-
switch (getElementById("root")) {
3+
switch (Bindings.getElementById("root")) {
84
| Some(el) => ReactDOMRe.hydrate(<App />, el)
95
| None => ()
106
};

dune-project

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
(routes (and (>= 0.8.0) (< 0.9.0)))
3333
(reason (>= 3.6.0))
3434
(uri (and (>= 3.1.0) (< 4.0.0)))
35+
(atdgen (and (>= 2.2.1) (< 2.3.0)))
3536

3637
;; HTML generation
3738
(tyxml :dev)

0 commit comments

Comments
 (0)