13 files changed,
+106,
-2997
+0,
-574
1@@ -1,574 +0,0 @@
2-// Generated by ReScript, PLEASE EDIT WITH CARE
3-
4-import * as Fetch from "./Fetch.res.mjs";
5-import * as Core__JSON from "@rescript/core/lib/es6/src/Core__JSON.res.mjs";
6-import * as Core__Array from "@rescript/core/lib/es6/src/Core__Array.res.mjs";
7-import * as Core__Option from "@rescript/core/lib/es6/src/Core__Option.res.mjs";
8-import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
9-
10-function $$encodeURIComponent(str) {
11- return encodeURIComponent(str);
12-}
13-
14-function getApiKey() {
15- let key = import.meta.env.VITE_STADIA_KEY;
16- if (key === undefined) {
17- return;
18- }
19- let s = Core__JSON.Decode.string(Primitive_option.valFromOption(key));
20- if (s !== undefined && s.length > 0) {
21- return s;
22- }
23-}
24-
25-function layerToString(layer) {
26- if (layer === "county") {
27- return "county";
28- } else if (layer === "macroregion") {
29- return "macroregion";
30- } else if (layer === "postalcode") {
31- return "postalcode";
32- } else if (layer === "locality") {
33- return "locality";
34- } else if (layer === "neighbourhood") {
35- return "neighbourhood";
36- } else if (layer === "poi") {
37- return "poi";
38- } else if (layer === "region") {
39- return "region";
40- } else if (layer === "street") {
41- return "street";
42- } else if (layer === "macrocounty") {
43- return "macrocounty";
44- } else if (layer === "address") {
45- return "address";
46- } else if (layer === "country") {
47- return "country";
48- } else if (layer === "borough") {
49- return "borough";
50- } else if (layer === "coarse") {
51- return "coarse";
52- } else {
53- return "localadmin";
54- }
55-}
56-
57-function parseLayer(str) {
58- switch (str) {
59- case "address" :
60- return "address";
61- case "borough" :
62- return "borough";
63- case "coarse" :
64- return "coarse";
65- case "country" :
66- return "country";
67- case "county" :
68- return "county";
69- case "localadmin" :
70- return "localadmin";
71- case "locality" :
72- return "locality";
73- case "macrocounty" :
74- return "macrocounty";
75- case "macroregion" :
76- return "macroregion";
77- case "neighbourhood" :
78- return "neighbourhood";
79- case "poi" :
80- return "poi";
81- case "postalcode" :
82- return "postalcode";
83- case "region" :
84- return "region";
85- case "street" :
86- return "street";
87- default:
88- return;
89- }
90-}
91-
92-function parseBaseProperties(props) {
93- let n = props["name"];
94- let name;
95- if (n !== undefined) {
96- let s = Core__JSON.Decode.string(n);
97- name = s !== undefined ? s : "";
98- } else {
99- name = "";
100- }
101- let g = props["gid"];
102- let gid;
103- if (g !== undefined) {
104- let s$1 = Core__JSON.Decode.string(g);
105- gid = s$1 !== undefined ? s$1 : "";
106- } else {
107- gid = "";
108- }
109- let l = props["layer"];
110- let layer;
111- if (l !== undefined) {
112- let s$2 = Core__JSON.Decode.string(l);
113- layer = s$2 !== undefined ? parseLayer(s$2) : undefined;
114- } else {
115- layer = undefined;
116- }
117- let c = props["coarse_location"];
118- let coarseLocation;
119- if (c !== undefined) {
120- let s$3 = Core__JSON.Decode.string(c);
121- coarseLocation = s$3 !== undefined ? s$3 : "";
122- } else {
123- coarseLocation = "";
124- }
125- return [
126- name,
127- gid,
128- layer,
129- coarseLocation
130- ];
131-}
132-
133-function checkGeocodingError(obj) {
134- let g = obj["geocoding"];
135- if (g === undefined) {
136- return;
137- }
138- let geoObj = Core__JSON.Decode.object(g);
139- if (geoObj === undefined) {
140- return;
141- }
142- let e = geoObj["error"];
143- if (e !== undefined) {
144- return Core__JSON.Decode.string(e);
145- }
146-}
147-
148-function handleStadiaResponse(json, parseFeature, wrapResults, emptyResponse, serverError) {
149- let obj = Core__JSON.Decode.object(json);
150- if (obj === undefined) {
151- return serverError("Invalid JSON response");
152- }
153- let errMsg = checkGeocodingError(obj);
154- if (errMsg !== undefined) {
155- return serverError(errMsg);
156- }
157- let f = obj["features"];
158- let features;
159- if (f !== undefined) {
160- let arr = Core__JSON.Decode.array(f);
161- features = arr !== undefined ? arr : [];
162- } else {
163- features = [];
164- }
165- let results = Core__Array.filterMap(features, parseFeature);
166- if (results.length === 0) {
167- return emptyResponse;
168- } else {
169- return wrapResults(results);
170- }
171-}
172-
173-function buildQueryString(apiKey, params) {
174- let parts = [];
175- parts.push(`text=` + $$encodeURIComponent(params.text));
176- let match = params.focusPoint;
177- if (match !== undefined) {
178- parts.push(`focus.point.lat=` + match[0].toString());
179- parts.push(`focus.point.lon=` + match[1].toString());
180- }
181- let match$1 = params.boundaryRect;
182- if (match$1 !== undefined) {
183- parts.push(`boundary.rect.min_lon=` + match$1[0].toString());
184- parts.push(`boundary.rect.max_lon=` + match$1[1].toString());
185- parts.push(`boundary.rect.min_lat=` + match$1[2].toString());
186- parts.push(`boundary.rect.max_lat=` + match$1[3].toString());
187- }
188- let match$2 = params.boundaryCircle;
189- if (match$2 !== undefined) {
190- parts.push(`boundary.circle.lat=` + match$2[0].toString());
191- parts.push(`boundary.circle.lon=` + match$2[1].toString());
192- parts.push(`boundary.circle.radius=` + match$2[2].toString());
193- }
194- let layers = params.layers;
195- if (layers !== undefined && layers.length > 0) {
196- let layerStr = layers.map(layerToString).join(",");
197- parts.push(`layers=` + $$encodeURIComponent(layerStr));
198- }
199- let sources = params.sources;
200- if (sources !== undefined && sources.length > 0) {
201- let sourceStr = sources.join(",");
202- parts.push(`sources=` + $$encodeURIComponent(sourceStr));
203- }
204- let countries = params.boundaryCountry;
205- if (countries !== undefined && countries.length > 0) {
206- let countryStr = countries.join(",");
207- parts.push(`boundary.country=` + $$encodeURIComponent(countryStr));
208- }
209- let gid = params.boundaryGid;
210- if (gid !== undefined) {
211- parts.push(`boundary.gid=` + $$encodeURIComponent(gid));
212- }
213- let size = params.size;
214- if (size !== undefined) {
215- parts.push(`size=` + size.toString());
216- }
217- let lang = params.lang;
218- if (lang !== undefined) {
219- parts.push(`lang=` + $$encodeURIComponent(lang));
220- }
221- if (apiKey !== undefined) {
222- parts.push(`api_key=` + $$encodeURIComponent(apiKey));
223- }
224- return parts.join("&");
225-}
226-
227-function parseFeature(feature) {
228- let obj = Core__JSON.Decode.object(feature);
229- if (obj === undefined) {
230- return;
231- }
232- let p = obj["properties"];
233- let properties = p !== undefined ? Core__JSON.Decode.object(p) : undefined;
234- if (properties === undefined) {
235- return;
236- }
237- let match = parseBaseProperties(properties);
238- let layer = match[2];
239- let d = properties["distance"];
240- let distance;
241- if (d !== undefined) {
242- let n = Core__JSON.Decode.float(d);
243- distance = n !== undefined ? n : undefined;
244- } else {
245- distance = undefined;
246- }
247- if (layer !== undefined) {
248- return {
249- name: match[0],
250- gid: match[1],
251- layer: layer,
252- coarseLocation: match[3],
253- distance: distance
254- };
255- }
256-}
257-
258-async function search(params) {
259- let apiKey = getApiKey();
260- let queryString = buildQueryString(apiKey, params);
261- let url = `https://api.stadiamaps.com/geocoding/v2/autocomplete?` + queryString;
262- try {
263- let response = await Fetch.send(Fetch.onResponse(Fetch.get(url), json => json, (status, message) => Object.fromEntries([
264- [
265- "__error",
266- true
267- ],
268- [
269- "__status",
270- status
271- ],
272- [
273- "__message",
274- message
275- ]
276- ])));
277- let obj = Core__JSON.Decode.object(response);
278- if (obj === undefined) {
279- return {
280- TAG: "ServerError",
281- _0: "Invalid JSON response"
282- };
283- }
284- let match = obj["__error"];
285- if (match === undefined) {
286- return handleStadiaResponse(response, parseFeature, results => ({
287- TAG: "Results",
288- _0: results
289- }), "Empty", msg => ({
290- TAG: "ServerError",
291- _0: msg
292- }));
293- }
294- let s = obj["__status"];
295- let status;
296- if (s !== undefined) {
297- let n = Core__JSON.Decode.float(s);
298- status = n !== undefined ? (n | 0).toString() : "0";
299- } else {
300- status = "0";
301- }
302- let m = obj["__message"];
303- let message;
304- if (m !== undefined) {
305- let s$1 = Core__JSON.Decode.string(m);
306- message = s$1 !== undefined ? s$1 : "Unknown error";
307- } else {
308- message = "Unknown error";
309- }
310- return {
311- TAG: "ServerError",
312- _0: status + `: ` + message
313- };
314- } catch (exn) {
315- return {
316- TAG: "ServerError",
317- _0: "Network error"
318- };
319- }
320-}
321-
322-function buildQueryString$1(apiKey, params) {
323- let parts = [];
324- parts.push(`ids=` + $$encodeURIComponent(params.gid));
325- let lang = params.lang;
326- if (lang !== undefined) {
327- parts.push(`lang=` + $$encodeURIComponent(lang));
328- }
329- if (apiKey !== undefined) {
330- parts.push(`api_key=` + $$encodeURIComponent(apiKey));
331- }
332- return parts.join("&");
333-}
334-
335-function parseFeature$1(feature) {
336- let obj = Core__JSON.Decode.object(feature);
337- if (obj === undefined) {
338- return;
339- }
340- let p = obj["properties"];
341- let properties = p !== undefined ? Core__JSON.Decode.object(p) : undefined;
342- let g = obj["geometry"];
343- let geometry = g !== undefined ? Core__JSON.Decode.object(g) : undefined;
344- if (properties === undefined) {
345- return;
346- }
347- if (geometry === undefined) {
348- return;
349- }
350- let match = parseBaseProperties(properties);
351- let layer = match[2];
352- let c = geometry["coordinates"];
353- let coordinates;
354- if (c !== undefined) {
355- let arr = Core__JSON.Decode.array(c);
356- if (arr !== undefined && arr.length >= 2) {
357- let lonJson = arr[0];
358- let lon;
359- if (lonJson !== undefined) {
360- let f = Core__JSON.Decode.float(lonJson);
361- lon = f !== undefined ? f : 0.0;
362- } else {
363- lon = 0.0;
364- }
365- let latJson = arr[1];
366- let lat;
367- if (latJson !== undefined) {
368- let f$1 = Core__JSON.Decode.float(latJson);
369- lat = f$1 !== undefined ? f$1 : 0.0;
370- } else {
371- lat = 0.0;
372- }
373- coordinates = [
374- lon,
375- lat
376- ];
377- } else {
378- coordinates = undefined;
379- }
380- } else {
381- coordinates = undefined;
382- }
383- let v = properties["formatted_address_line"];
384- let formattedAddressLine = v !== undefined ? Core__JSON.Decode.string(v) : undefined;
385- let v$1 = properties["formatted_address_lines"];
386- let formattedAddressLines;
387- if (v$1 !== undefined) {
388- let arr$1 = Core__JSON.Decode.array(v$1);
389- formattedAddressLines = arr$1 !== undefined ? Core__Array.filterMap(arr$1, item => Core__JSON.Decode.string(item)) : undefined;
390- } else {
391- formattedAddressLines = undefined;
392- }
393- let ac = properties["address_components"];
394- let addressComponents;
395- if (ac !== undefined) {
396- let obj$1 = Core__JSON.Decode.object(ac);
397- if (obj$1 !== undefined) {
398- let number = Core__Option.flatMap(obj$1["number"], Core__JSON.Decode.string);
399- let street = Core__Option.flatMap(obj$1["street"], Core__JSON.Decode.string);
400- let postal_code = Core__Option.flatMap(obj$1["postal_code"], Core__JSON.Decode.string);
401- addressComponents = {
402- number: number,
403- street: street,
404- postal_code: postal_code
405- };
406- } else {
407- addressComponents = undefined;
408- }
409- } else {
410- addressComponents = undefined;
411- }
412- let ctx = properties["context"];
413- let context;
414- if (ctx !== undefined) {
415- let ctxObj = Core__JSON.Decode.object(ctx);
416- if (ctxObj !== undefined) {
417- let parseEntity = key => {
418- let e = ctxObj[key];
419- if (e === undefined) {
420- return;
421- }
422- let eObj = Core__JSON.Decode.object(e);
423- if (eObj === undefined) {
424- return;
425- }
426- let gidOpt = Core__Option.flatMap(eObj["gid"], Core__JSON.Decode.string);
427- let nameOpt = Core__Option.flatMap(eObj["name"], Core__JSON.Decode.string);
428- if (gidOpt !== undefined && nameOpt !== undefined) {
429- return {
430- gid: gidOpt,
431- name: nameOpt
432- };
433- }
434- };
435- let country = parseEntity("country");
436- let region = parseEntity("region");
437- let county = parseEntity("county");
438- let locality = parseEntity("locality");
439- let neighbourhood = parseEntity("neighbourhood");
440- let whosonfirst;
441- let exit = 0;
442- if (country !== undefined || region !== undefined || county !== undefined || locality !== undefined || neighbourhood !== undefined) {
443- exit = 1;
444- } else {
445- whosonfirst = undefined;
446- }
447- if (exit === 1) {
448- whosonfirst = {
449- country: country,
450- region: region,
451- county: county,
452- locality: locality,
453- neighbourhood: neighbourhood
454- };
455- }
456- let iso_3166_a2 = Core__Option.flatMap(ctxObj["iso_3166_a2"], Core__JSON.Decode.string);
457- let iso_3166_a3 = Core__Option.flatMap(ctxObj["iso_3166_a3"], Core__JSON.Decode.string);
458- let exit$1 = 0;
459- if (whosonfirst !== undefined || iso_3166_a2 !== undefined || iso_3166_a3 !== undefined) {
460- exit$1 = 1;
461- } else {
462- context = undefined;
463- }
464- if (exit$1 === 1) {
465- context = {
466- whosonfirst: whosonfirst,
467- iso_3166_a2: iso_3166_a2,
468- iso_3166_a3: iso_3166_a3
469- };
470- }
471- } else {
472- context = undefined;
473- }
474- } else {
475- context = undefined;
476- }
477- if (layer !== undefined && coordinates !== undefined) {
478- return {
479- name: match[0],
480- gid: match[1],
481- layer: layer,
482- coarseLocation: match[3],
483- coordinates: coordinates,
484- formattedAddressLine: formattedAddressLine,
485- formattedAddressLines: formattedAddressLines,
486- addressComponents: addressComponents,
487- context: context
488- };
489- }
490-}
491-
492-async function getDetails(params) {
493- let apiKey = getApiKey();
494- let queryString = buildQueryString$1(apiKey, params);
495- let url = `https://api.stadiamaps.com/geocoding/v2/place_details?` + queryString;
496- try {
497- let response = await Fetch.send(Fetch.onResponse(Fetch.get(url), json => json, (status, message) => Object.fromEntries([
498- [
499- "__error",
500- true
501- ],
502- [
503- "__status",
504- status
505- ],
506- [
507- "__message",
508- message
509- ]
510- ])));
511- let obj = Core__JSON.Decode.object(response);
512- if (obj === undefined) {
513- return {
514- TAG: "ServerError",
515- _0: "Invalid JSON response"
516- };
517- }
518- let match = obj["__error"];
519- if (match === undefined) {
520- return handleStadiaResponse(response, parseFeature$1, results => {
521- let result = results[0];
522- if (result !== undefined) {
523- return {
524- TAG: "Found",
525- _0: result
526- };
527- } else {
528- return "NotFound";
529- }
530- }, "NotFound", msg => ({
531- TAG: "ServerError",
532- _0: msg
533- }));
534- }
535- let s = obj["__status"];
536- let status;
537- if (s !== undefined) {
538- let n = Core__JSON.Decode.float(s);
539- status = n !== undefined ? (n | 0).toString() : "0";
540- } else {
541- status = "0";
542- }
543- let m = obj["__message"];
544- let message;
545- if (m !== undefined) {
546- let s$1 = Core__JSON.Decode.string(m);
547- message = s$1 !== undefined ? s$1 : "Unknown error";
548- } else {
549- message = "Unknown error";
550- }
551- return {
552- TAG: "ServerError",
553- _0: status + `: ` + message
554- };
555- } catch (exn) {
556- return {
557- TAG: "ServerError",
558- _0: "Network error"
559- };
560- }
561-}
562-
563-let Autocomplete = {
564- search: search
565-};
566-
567-let Place = {
568- getDetails: getDetails
569-};
570-
571-export {
572- Autocomplete,
573- Place,
574-}
575-/* Fetch Not a pure module */
+2,
-9
1@@ -1,5 +1,6 @@
2 // Generated by ReScript, PLEASE EDIT WITH CARE
3
4+import * as Log from "../utils/Log.res.mjs";
5 import * as Fetch from "./Fetch.res.mjs";
6 import * as Core__JSON from "@rescript/core/lib/es6/src/Core__JSON.res.mjs";
7 import * as Core__Array from "@rescript/core/lib/es6/src/Core__Array.res.mjs";
8@@ -74,11 +75,9 @@ function decodeResult(json) {
9 function decodeGeocodeResponse(json) {
10 let dict = Core__JSON.Decode.object(json);
11 if (dict !== undefined) {
12- console.log("[Waldo] decodeGeocodeResponse: Got JSON object, extracting fields...");
13 let results = Core__Option.map(Core__Option.flatMap(dict["results"], Core__JSON.Decode.array), arr => Core__Array.filterMap(arr, decodeResult));
14 let numResults = Core__Option.map(Core__Option.flatMap(dict["num_results"], Core__JSON.Decode.float), f => f | 0);
15 if (results !== undefined && numResults !== undefined) {
16- console.log(`[Waldo] decodeGeocodeResponse: Success with ` + numResults.toString() + ` results`);
17 return {
18 results: results,
19 numResults: numResults
20@@ -124,7 +123,6 @@ function buildUrl(params) {
21 async function geocode(params) {
22 let url = buildUrl(params);
23 try {
24- console.log("[Waldo] Forward: Starting fetch request...");
25 let response = await Fetch.send(Fetch.onResponse(Fetch.get(url), json => json, (status, message) => Object.fromEntries([
26 [
27 "__error",
28@@ -139,17 +137,15 @@ async function geocode(params) {
29 message
30 ]
31 ])));
32- console.log("[Waldo] Forward: Raw response received");
33 let responseStr;
34 try {
35 responseStr = JSON.stringify(response);
36 } catch (exn) {
37 responseStr = "[Failed to stringify response]";
38 }
39- console.log(`[Waldo] Forward: Response content: ` + responseStr);
40+ Log.debug(`[Waldo] Forward: Response content: ` + responseStr, undefined);
41 let obj = Core__JSON.Decode.object(response);
42 if (obj !== undefined) {
43- console.log("[Waldo] Forward: Response is JSON object");
44 let match = obj["__error"];
45 if (match !== undefined) {
46 let s = obj["__status"];
47@@ -174,11 +170,8 @@ async function geocode(params) {
48 _0: status + `: ` + message
49 };
50 }
51- console.log("[Waldo] Forward: No __error field found, attempting to decode geocode response...");
52- console.log(`[Waldo] Forward: Response object keys: ` + Object.keys(obj).join(", "));
53 let resp = decodeGeocodeResponse(response);
54 if (resp !== undefined) {
55- console.log(`[Waldo] Forward: Decoded ` + resp.numResults.toString() + ` results`);
56 return {
57 TAG: "Success",
58 _0: resp.results
1@@ -0,0 +1,2 @@
2+// Generated by ReScript, PLEASE EDIT WITH CARE
3+/* This output is empty. Its source's type definitions, externals and/or unused code got optimized away. */
+1,
-6
1@@ -6,7 +6,6 @@ import * as NanoID from "../ffi/NanoID.res.mjs";
2 import * as Preact from "../ffi/Preact.res.mjs";
3 import * as Tiptap from "../ffi/Tiptap.res.mjs";
4 import * as PreactDOM from "../ffi/PreactDOM.res.mjs";
5-import * as ContentJs from "./content.js";
6 import * as Core from "@tiptap/core";
7 import * as Core__Option from "@rescript/core/lib/es6/src/Core__Option.res.mjs";
8 import * as React from "@tiptap/react";
9@@ -15,8 +14,6 @@ import * as Signals from "@preact/signals";
10 import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
11 import * as JsxRuntime from "preact/jsx-runtime";
12
13-let content = ContentJs.content;
14-
15 let FloatingUI = {};
16
17 let Instance = {};
18@@ -52,7 +49,6 @@ function Editor(props) {
19 }
20 },
21 extensions: Tiptap.extensions(ydoc, doc),
22- content: content,
23 onUpdate: _event => {
24 let ed = editorRef.contents;
25 if (ed === undefined) {
26@@ -187,7 +183,6 @@ let FloatingMenu = {};
27 let make = Editor;
28
29 export {
30- content,
31 FloatingUI,
32 Instance,
33 onSearchSelectCache,
34@@ -196,4 +191,4 @@ export {
35 BubbleMenu,
36 FloatingMenu,
37 }
38-/* content Not a pure module */
39+/* onSearchSelectCache Not a pure module */
+0,
-19
1@@ -1,19 +0,0 @@
2-export const content = {
3- type: "doc",
4- content: [
5- {
6- type: "heading",
7- attrs: { level: 1 },
8- content: [{ type: "text", text: "Start with a title..." }],
9- },
10- {
11- type: "paragraph",
12- content: [
13- {
14- type: "text",
15- text: "To add a waypoint type @ or click on the menu button at the start of an empty line...",
16- },
17- ],
18- },
19- ],
20-};
+0,
-902
1@@ -1,902 +0,0 @@
2-// Generated by ReScript, PLEASE EDIT WITH CARE
3-
4-import * as Stadia from "../src/ffi/Stadia.res.mjs";
5-import * as Vitest from "vitest";
6-import * as Core__Option from "@rescript/core/lib/es6/src/Core__Option.res.mjs";
7-
8-let mockFetchJson = (function(json, status, statusText) {
9- globalThis.fetch = vi.fn(() => Promise.resolve({
10- ok: status >= 200 && status < 300,
11- status: status,
12- statusText: statusText,
13- json: () => Promise.resolve(json)
14- }));
15- });
16-
17-let setupMockWithCapture = (function() {
18- const calls = [];
19- globalThis.fetch = vi.fn((url, init) => {
20- calls.push({url, init});
21- return Promise.resolve({
22- ok: true,
23- status: 200,
24- statusText: 'OK',
25- json: () => Promise.resolve({
26- type: 'FeatureCollection',
27- features: [],
28- geocoding: { attribution: 'test' }
29- })
30- });
31- });
32- return calls;
33- });
34-
35-let getUrl = (function(calls) {
36- return calls[0].url;
37- });
38-
39-Vitest.describe("Stadia.Autocomplete", () => {
40- Vitest.test("returns Results with parsed data for successful search", async t => {
41- let responseJson = Object.fromEntries([
42- [
43- "type",
44- "FeatureCollection"
45- ],
46- [
47- "geocoding",
48- Object.fromEntries([
49- [
50- "attribution",
51- "https://stadiamaps.com/attribution/"
52- ],
53- [
54- "query",
55- Object.fromEntries([[
56- "text",
57- "Põhja pst 27"
58- ]])
59- ]
60- ])
61- ],
62- [
63- "features",
64- [
65- Object.fromEntries([
66- [
67- "type",
68- "Feature"
69- ],
70- [
71- "geometry",
72- null
73- ],
74- [
75- "properties",
76- Object.fromEntries([
77- [
78- "name",
79- "Põhja Puiestee 27"
80- ],
81- [
82- "gid",
83- "openaddresses:address:ee/countrywide-addresses-country:5cbc00647aa5c976"
84- ],
85- [
86- "layer",
87- "address"
88- ],
89- [
90- "coarse_location",
91- "Tallinn, Harju, Estonia"
92- ],
93- [
94- "precision",
95- "point"
96- ]
97- ])
98- ]
99- ]),
100- Object.fromEntries([
101- [
102- "type",
103- "Feature"
104- ],
105- [
106- "geometry",
107- null
108- ],
109- [
110- "properties",
111- Object.fromEntries([
112- [
113- "name",
114- "Põhja pst 27"
115- ],
116- [
117- "gid",
118- "openstreetmap:address:way/224094258"
119- ],
120- [
121- "layer",
122- "address"
123- ],
124- [
125- "coarse_location",
126- "Viljandi, Estonia"
127- ],
128- [
129- "precision",
130- "centroid"
131- ]
132- ])
133- ]
134- ])
135- ]
136- ]
137- ]);
138- mockFetchJson(responseJson, 200, "OK");
139- let result = await Stadia.Autocomplete.search({
140- text: "Põhja pst 27"
141- });
142- if (typeof result !== "object") {
143- t.expect(true).toEqual(false);
144- return;
145- }
146- if (result.TAG === "Results") {
147- let results = result._0;
148- t.expect(results.length).toEqual(2);
149- let first = results[0];
150- if (first !== undefined) {
151- t.expect(first.name).toEqual("Põhja Puiestee 27");
152- t.expect(first.gid).toEqual("openaddresses:address:ee/countrywide-addresses-country:5cbc00647aa5c976");
153- t.expect(first.layer).toEqual("address");
154- t.expect(first.coarseLocation).toEqual("Tallinn, Harju, Estonia");
155- t.expect(first.distance).toEqual(undefined);
156- } else {
157- t.expect(true).toEqual(false);
158- }
159- let second = results[1];
160- if (second !== undefined) {
161- t.expect(second.name).toEqual("Põhja pst 27");
162- t.expect(second.layer).toEqual("address");
163- } else {
164- t.expect(true).toEqual(false);
165- }
166- return;
167- }
168- t.expect(true).toEqual(false);
169- });
170- Vitest.test("returns Results with distance when provided", async t => {
171- let responseJson = Object.fromEntries([
172- [
173- "type",
174- "FeatureCollection"
175- ],
176- [
177- "geocoding",
178- Object.fromEntries([[
179- "attribution",
180- "test"
181- ]])
182- ],
183- [
184- "features",
185- [Object.fromEntries([
186- [
187- "type",
188- "Feature"
189- ],
190- [
191- "geometry",
192- null
193- ],
194- [
195- "properties",
196- Object.fromEntries([
197- [
198- "name",
199- "Test Location"
200- ],
201- [
202- "gid",
203- "test:123"
204- ],
205- [
206- "layer",
207- "address"
208- ],
209- [
210- "coarse_location",
211- "City, Country"
212- ],
213- [
214- "distance",
215- 1.5
216- ]
217- ])
218- ]
219- ])]
220- ]
221- ]);
222- mockFetchJson(responseJson, 200, "OK");
223- let result = await Stadia.Autocomplete.search({
224- text: "test",
225- focusPoint: [
226- 59.437,
227- 24.753
228- ]
229- });
230- if (typeof result !== "object") {
231- t.expect(true).toEqual(false);
232- return;
233- }
234- if (result.TAG === "Results") {
235- let results = result._0;
236- t.expect(results.length).toEqual(1);
237- let first = results[0];
238- if (first !== undefined) {
239- t.expect(first.distance).toEqual(1.5);
240- } else {
241- t.expect(true).toEqual(false);
242- }
243- return;
244- }
245- t.expect(true).toEqual(false);
246- });
247- Vitest.test("returns Empty for empty features array", async t => {
248- let responseJson = Object.fromEntries([
249- [
250- "type",
251- "FeatureCollection"
252- ],
253- [
254- "geocoding",
255- Object.fromEntries([[
256- "attribution",
257- "test"
258- ]])
259- ],
260- [
261- "features",
262- []
263- ]
264- ]);
265- mockFetchJson(responseJson, 200, "OK");
266- let result = await Stadia.Autocomplete.search({
267- text: "xyznonexistent12345"
268- });
269- t.expect(result).toEqual("Empty");
270- });
271- Vitest.test("returns ServerError on HTTP error", async t => {
272- let errorJson = Object.fromEntries([[
273- "error",
274- "Invalid API key"
275- ]]);
276- mockFetchJson(errorJson, 401, "Unauthorized");
277- let result = await Stadia.Autocomplete.search({
278- text: "test"
279- });
280- if (typeof result !== "object") {
281- t.expect(true).toEqual(false);
282- return;
283- }
284- if (result.TAG === "Results") {
285- t.expect(true).toEqual(false);
286- return;
287- }
288- t.expect(result._0).toEqual("401: Invalid API key");
289- });
290- Vitest.test("returns ServerError on geocoding error", async t => {
291- let responseJson = Object.fromEntries([
292- [
293- "type",
294- "FeatureCollection"
295- ],
296- [
297- "geocoding",
298- Object.fromEntries([
299- [
300- "attribution",
301- "test"
302- ],
303- [
304- "error",
305- "Invalid parameter: text is required"
306- ]
307- ])
308- ],
309- [
310- "features",
311- []
312- ]
313- ]);
314- mockFetchJson(responseJson, 200, "OK");
315- let result = await Stadia.Autocomplete.search({
316- text: "test"
317- });
318- if (typeof result !== "object") {
319- t.expect(true).toEqual(false);
320- return;
321- }
322- if (result.TAG === "Results") {
323- t.expect(true).toEqual(false);
324- return;
325- }
326- t.expect(result._0).toEqual("Invalid parameter: text is required");
327- });
328- Vitest.test("encodes text parameter in query string", async t => {
329- let _calls = setupMockWithCapture();
330- await Stadia.Autocomplete.search({
331- text: "Põhja pst 27"
332- });
333- let url = getUrl(_calls);
334- t.expect(url.includes("text=P%C3%B5hja%20pst%2027")).toEqual(true);
335- });
336- Vitest.test("encodes focus point parameters", async t => {
337- let _calls = setupMockWithCapture();
338- await Stadia.Autocomplete.search({
339- text: "test",
340- focusPoint: [
341- 59.437,
342- 24.753
343- ]
344- });
345- let url = getUrl(_calls);
346- t.expect(url.includes("focus.point.lat=59.437")).toEqual(true);
347- t.expect(url.includes("focus.point.lon=24.753")).toEqual(true);
348- });
349- Vitest.test("encodes boundary rect parameters", async t => {
350- let _calls = setupMockWithCapture();
351- await Stadia.Autocomplete.search({
352- text: "test",
353- boundaryRect: [
354- 139.2794,
355- 140.1471,
356- 35.53308,
357- 35.81346
358- ]
359- });
360- let url = getUrl(_calls);
361- t.expect(url.includes("boundary.rect.min_lon=139.2794")).toEqual(true);
362- t.expect(url.includes("boundary.rect.max_lon=140.1471")).toEqual(true);
363- t.expect(url.includes("boundary.rect.min_lat=35.53308")).toEqual(true);
364- t.expect(url.includes("boundary.rect.max_lat=35.81346")).toEqual(true);
365- });
366- Vitest.test("encodes boundary circle parameters", async t => {
367- let _calls = setupMockWithCapture();
368- await Stadia.Autocomplete.search({
369- text: "test",
370- boundaryCircle: [
371- 43.818156,
372- -79.186484,
373- 35.0
374- ]
375- });
376- let url = getUrl(_calls);
377- t.expect(url.includes("boundary.circle.lat=43.818156")).toEqual(true);
378- t.expect(url.includes("boundary.circle.lon=-79.186484")).toEqual(true);
379- t.expect(url.includes("boundary.circle.radius=35")).toEqual(true);
380- });
381- Vitest.test("encodes layers as comma-separated", async t => {
382- let _calls = setupMockWithCapture();
383- await Stadia.Autocomplete.search({
384- text: "test",
385- layers: [
386- "address",
387- "poi",
388- "coarse"
389- ]
390- });
391- let url = getUrl(_calls);
392- t.expect(url.includes("layers=address%2Cpoi%2Ccoarse") || url.includes("layers=address,poi,coarse")).toEqual(true);
393- });
394- Vitest.test("encodes sources as comma-separated", async t => {
395- let _calls = setupMockWithCapture();
396- await Stadia.Autocomplete.search({
397- text: "test",
398- sources: [
399- "openstreetmap",
400- "openaddresses"
401- ]
402- });
403- let url = getUrl(_calls);
404- t.expect(url.includes("sources=openstreetmap%2Copenaddresses") || url.includes("sources=openstreetmap,openaddresses")).toEqual(true);
405- });
406- Vitest.test("encodes boundary country as comma-separated", async t => {
407- let _calls = setupMockWithCapture();
408- await Stadia.Autocomplete.search({
409- text: "test",
410- boundaryCountry: [
411- "GBR",
412- "FRA"
413- ]
414- });
415- let url = getUrl(_calls);
416- t.expect(url.includes("boundary.country=GBR%2CFRA") || url.includes("boundary.country=GBR,FRA")).toEqual(true);
417- });
418- Vitest.test("encodes boundary gid", async t => {
419- let _calls = setupMockWithCapture();
420- await Stadia.Autocomplete.search({
421- text: "test",
422- boundaryGid: "whosonfirst:locality:101748355"
423- });
424- let url = getUrl(_calls);
425- t.expect(url.includes("boundary.gid=whosonfirst%3Alocality%3A101748355")).toEqual(true);
426- });
427- Vitest.test("encodes size parameter", async t => {
428- let _calls = setupMockWithCapture();
429- await Stadia.Autocomplete.search({
430- text: "test",
431- size: 5
432- });
433- let url = getUrl(_calls);
434- t.expect(url.includes("size=5")).toEqual(true);
435- });
436- Vitest.test("encodes lang parameter", async t => {
437- let _calls = setupMockWithCapture();
438- await Stadia.Autocomplete.search({
439- text: "test",
440- lang: "en"
441- });
442- let url = getUrl(_calls);
443- t.expect(url.includes("lang=en")).toEqual(true);
444- });
445- Vitest.test("includes api_key in URL when env var is present", async t => {
446- let _calls = setupMockWithCapture();
447- await Stadia.Autocomplete.search({
448- text: "test"
449- });
450- let url = getUrl(_calls);
451- t.expect(url.length > 0).toEqual(true);
452- t.expect(url.includes("https://api.stadiamaps.com/geocoding/v2/autocomplete")).toEqual(true);
453- });
454- Vitest.test("handles all layer types correctly", async t => {
455- let testCases = [
456- [
457- "poi",
458- "poi"
459- ],
460- [
461- "address",
462- "address"
463- ],
464- [
465- "street",
466- "street"
467- ],
468- [
469- "country",
470- "country"
471- ],
472- [
473- "macroregion",
474- "macroregion"
475- ],
476- [
477- "region",
478- "region"
479- ],
480- [
481- "macrocounty",
482- "macrocounty"
483- ],
484- [
485- "county",
486- "county"
487- ],
488- [
489- "locality",
490- "locality"
491- ],
492- [
493- "localadmin",
494- "localadmin"
495- ],
496- [
497- "borough",
498- "borough"
499- ],
500- [
501- "neighbourhood",
502- "neighbourhood"
503- ],
504- [
505- "postalcode",
506- "postalcode"
507- ],
508- [
509- "coarse",
510- "coarse"
511- ]
512- ];
513- let features = testCases.map((param, idx) => Object.fromEntries([
514- [
515- "type",
516- "Feature"
517- ],
518- [
519- "geometry",
520- null
521- ],
522- [
523- "properties",
524- Object.fromEntries([
525- [
526- "name",
527- `Test ` + idx.toString()
528- ],
529- [
530- "gid",
531- `test:` + idx.toString()
532- ],
533- [
534- "layer",
535- param[0]
536- ],
537- [
538- "coarse_location",
539- "Test City"
540- ]
541- ])
542- ]
543- ]));
544- let responseJson = Object.fromEntries([
545- [
546- "type",
547- "FeatureCollection"
548- ],
549- [
550- "geocoding",
551- Object.fromEntries([[
552- "attribution",
553- "test"
554- ]])
555- ],
556- [
557- "features",
558- features
559- ]
560- ]);
561- mockFetchJson(responseJson, 200, "OK");
562- let result = await Stadia.Autocomplete.search({
563- text: "test"
564- });
565- if (typeof result !== "object") {
566- t.expect(true).toEqual(false);
567- return;
568- }
569- if (result.TAG === "Results") {
570- let results = result._0;
571- t.expect(results.length).toEqual(testCases.length);
572- testCases.forEach((param, idx) => {
573- t.expect(Core__Option.map(results[idx], r => r.layer)).toEqual(param[1]);
574- });
575- return;
576- }
577- t.expect(true).toEqual(false);
578- });
579-});
580-
581-Vitest.describe("Stadia.Place", () => {
582- Vitest.test("returns Found with coordinates for valid gid", async t => {
583- let responseJson = Object.fromEntries([
584- [
585- "type",
586- "FeatureCollection"
587- ],
588- [
589- "geocoding",
590- Object.fromEntries([[
591- "attribution",
592- "https://stadiamaps.com/attribution/"
593- ]])
594- ],
595- [
596- "features",
597- [Object.fromEntries([
598- [
599- "type",
600- "Feature"
601- ],
602- [
603- "geometry",
604- Object.fromEntries([
605- [
606- "type",
607- "Point"
608- ],
609- [
610- "coordinates",
611- [
612- 24.753,
613- 59.437
614- ]
615- ]
616- ])
617- ],
618- [
619- "properties",
620- Object.fromEntries([
621- [
622- "name",
623- "Põhja Puiestee 27"
624- ],
625- [
626- "gid",
627- "openaddresses:address:ee/countrywide-addresses-country:5cbc00647aa5c976"
628- ],
629- [
630- "layer",
631- "address"
632- ],
633- [
634- "coarse_location",
635- "Tallinn, Harju, Estonia"
636- ]
637- ])
638- ]
639- ])]
640- ]
641- ]);
642- mockFetchJson(responseJson, 200, "OK");
643- let result = await Stadia.Place.getDetails({
644- gid: "openaddresses:address:ee/countrywide-addresses-country:5cbc00647aa5c976"
645- });
646- if (typeof result !== "object") {
647- t.expect(true).toEqual(false);
648- return;
649- }
650- if (result.TAG === "Found") {
651- let place = result._0;
652- t.expect(place.name).toEqual("Põhja Puiestee 27");
653- t.expect(place.gid).toEqual("openaddresses:address:ee/countrywide-addresses-country:5cbc00647aa5c976");
654- t.expect(place.layer).toEqual("address");
655- t.expect(place.coarseLocation).toEqual("Tallinn, Harju, Estonia");
656- let match = place.coordinates;
657- t.expect(match[0]).toEqual(24.753);
658- t.expect(match[1]).toEqual(59.437);
659- return;
660- }
661- t.expect(true).toEqual(false);
662- });
663- Vitest.test("returns NotFound for empty features", async t => {
664- let responseJson = Object.fromEntries([
665- [
666- "type",
667- "FeatureCollection"
668- ],
669- [
670- "geocoding",
671- Object.fromEntries([[
672- "attribution",
673- "test"
674- ]])
675- ],
676- [
677- "features",
678- []
679- ]
680- ]);
681- mockFetchJson(responseJson, 200, "OK");
682- let result = await Stadia.Place.getDetails({
683- gid: "nonexistent:123"
684- });
685- t.expect(result).toEqual("NotFound");
686- });
687- Vitest.test("returns ServerError on HTTP error", async t => {
688- let errorJson = Object.fromEntries([[
689- "error",
690- "Not found"
691- ]]);
692- mockFetchJson(errorJson, 404, "Not Found");
693- let result = await Stadia.Place.getDetails({
694- gid: "test:123"
695- });
696- if (typeof result !== "object") {
697- t.expect(true).toEqual(false);
698- return;
699- }
700- if (result.TAG === "Found") {
701- t.expect(true).toEqual(false);
702- return;
703- }
704- t.expect(result._0).toEqual("404: Not found");
705- });
706- Vitest.test("returns ServerError on geocoding error", async t => {
707- let responseJson = Object.fromEntries([
708- [
709- "type",
710- "FeatureCollection"
711- ],
712- [
713- "geocoding",
714- Object.fromEntries([
715- [
716- "attribution",
717- "test"
718- ],
719- [
720- "error",
721- "Invalid GID format"
722- ]
723- ])
724- ],
725- [
726- "features",
727- []
728- ]
729- ]);
730- mockFetchJson(responseJson, 200, "OK");
731- let result = await Stadia.Place.getDetails({
732- gid: "invalid"
733- });
734- if (typeof result !== "object") {
735- t.expect(true).toEqual(false);
736- return;
737- }
738- if (result.TAG === "Found") {
739- t.expect(true).toEqual(false);
740- return;
741- }
742- t.expect(result._0).toEqual("Invalid GID format");
743- });
744- Vitest.test("encodes gid in ids parameter", async t => {
745- let _calls = setupMockWithCapture();
746- await Stadia.Place.getDetails({
747- gid: "openstreetmap:address:way/123"
748- });
749- let url = getUrl(_calls);
750- t.expect(url.includes("ids=openstreetmap%3Aaddress%3Away%2F123")).toEqual(true);
751- });
752- Vitest.test("encodes lang parameter", async t => {
753- let _calls = setupMockWithCapture();
754- await Stadia.Place.getDetails({
755- gid: "test:123",
756- lang: "en"
757- });
758- let url = getUrl(_calls);
759- t.expect(url.includes("lang=en")).toEqual(true);
760- });
761- Vitest.test("handles all layer types", async t => {
762- let testCases = [
763- [
764- "poi",
765- "poi"
766- ],
767- [
768- "address",
769- "address"
770- ],
771- [
772- "street",
773- "street"
774- ],
775- [
776- "country",
777- "country"
778- ],
779- [
780- "macroregion",
781- "macroregion"
782- ],
783- [
784- "region",
785- "region"
786- ],
787- [
788- "macrocounty",
789- "macrocounty"
790- ],
791- [
792- "county",
793- "county"
794- ],
795- [
796- "locality",
797- "locality"
798- ],
799- [
800- "localadmin",
801- "localadmin"
802- ],
803- [
804- "borough",
805- "borough"
806- ],
807- [
808- "neighbourhood",
809- "neighbourhood"
810- ],
811- [
812- "postalcode",
813- "postalcode"
814- ],
815- [
816- "coarse",
817- "coarse"
818- ]
819- ];
820- let features = testCases.map((param, idx) => Object.fromEntries([
821- [
822- "type",
823- "Feature"
824- ],
825- [
826- "geometry",
827- Object.fromEntries([
828- [
829- "type",
830- "Point"
831- ],
832- [
833- "coordinates",
834- [
835- idx,
836- idx
837- ]
838- ]
839- ])
840- ],
841- [
842- "properties",
843- Object.fromEntries([
844- [
845- "name",
846- `Test ` + idx.toString()
847- ],
848- [
849- "gid",
850- `test:` + idx.toString()
851- ],
852- [
853- "layer",
854- param[0]
855- ],
856- [
857- "coarse_location",
858- "Test City"
859- ]
860- ])
861- ]
862- ]));
863- let f = features[0];
864- let firstFeature = f !== undefined ? f : null;
865- let responseJson = Object.fromEntries([
866- [
867- "type",
868- "FeatureCollection"
869- ],
870- [
871- "geocoding",
872- Object.fromEntries([[
873- "attribution",
874- "test"
875- ]])
876- ],
877- [
878- "features",
879- [firstFeature]
880- ]
881- ]);
882- mockFetchJson(responseJson, 200, "OK");
883- let result = await Stadia.Place.getDetails({
884- gid: "test:0"
885- });
886- if (typeof result !== "object") {
887- t.expect(true).toEqual(false);
888- return;
889- }
890- if (result.TAG === "Found") {
891- t.expect(result._0.layer).toEqual("poi");
892- return;
893- }
894- t.expect(true).toEqual(false);
895- });
896-});
897-
898-export {
899- mockFetchJson,
900- setupMockWithCapture,
901- getUrl,
902-}
903-/* Not a pure module */
+0,
-699
1@@ -1,699 +0,0 @@
2-// Internal helpers (not exposed in .resi)
3-
4-let encodeURIComponent = (str: string): string => {
5- %raw(`encodeURIComponent`)(str)
6-}
7-
8-let getApiKey = (): option<string> => {
9- let key = %raw(`import.meta.env.VITE_STADIA_KEY`)
10- switch key {
11- | None => None
12- | Some(k) =>
13- switch k->JSON.Decode.string {
14- | Some(s) if s->String.length > 0 => Some(s)
15- | _ => None
16- }
17- }
18-}
19-
20-// Shared layer type and conversion functions
21-type layer = [
22- | #poi
23- | #address
24- | #street
25- | #country
26- | #macroregion
27- | #region
28- | #macrocounty
29- | #county
30- | #locality
31- | #localadmin
32- | #borough
33- | #neighbourhood
34- | #postalcode
35- | #coarse
36-]
37-
38-let layerToString = (layer: layer): string => {
39- switch layer {
40- | #poi => "poi"
41- | #address => "address"
42- | #street => "street"
43- | #country => "country"
44- | #macroregion => "macroregion"
45- | #region => "region"
46- | #macrocounty => "macrocounty"
47- | #county => "county"
48- | #locality => "locality"
49- | #localadmin => "localadmin"
50- | #borough => "borough"
51- | #neighbourhood => "neighbourhood"
52- | #postalcode => "postalcode"
53- | #coarse => "coarse"
54- }
55-}
56-
57-let parseLayer = (str: string): option<layer> => {
58- switch str {
59- | "poi" => Some(#poi)
60- | "address" => Some(#address)
61- | "street" => Some(#street)
62- | "country" => Some(#country)
63- | "macroregion" => Some(#macroregion)
64- | "region" => Some(#region)
65- | "macrocounty" => Some(#macrocounty)
66- | "county" => Some(#county)
67- | "locality" => Some(#locality)
68- | "localadmin" => Some(#localadmin)
69- | "borough" => Some(#borough)
70- | "neighbourhood" => Some(#neighbourhood)
71- | "postalcode" => Some(#postalcode)
72- | "coarse" => Some(#coarse)
73- | _ => None
74- }
75-}
76-
77-// Parse common properties from feature properties object
78-let parseBaseProperties = (props: Dict.t<JSON.t>): (string, string, option<layer>, string) => {
79- // Extract name
80- let name = switch props->Dict.get("name") {
81- | Some(n) =>
82- switch n->JSON.Decode.string {
83- | Some(s) => s
84- | None => ""
85- }
86- | None => ""
87- }
88-
89- // Extract gid
90- let gid = switch props->Dict.get("gid") {
91- | Some(g) =>
92- switch g->JSON.Decode.string {
93- | Some(s) => s
94- | None => ""
95- }
96- | None => ""
97- }
98-
99- // Extract layer
100- let layer = switch props->Dict.get("layer") {
101- | Some(l) =>
102- switch l->JSON.Decode.string {
103- | Some(s) => parseLayer(s)
104- | None => None
105- }
106- | None => None
107- }
108-
109- // Extract coarse_location
110- let coarseLocation = switch props->Dict.get("coarse_location") {
111- | Some(c) =>
112- switch c->JSON.Decode.string {
113- | Some(s) => s
114- | None => ""
115- }
116- | None => ""
117- }
118-
119- (name, gid, layer, coarseLocation)
120-}
121-
122-// Check for geocoding error in response
123-let checkGeocodingError = (obj: Dict.t<JSON.t>): option<string> => {
124- switch obj->Dict.get("geocoding") {
125- | Some(g) =>
126- switch g->JSON.Decode.object {
127- | Some(geoObj) =>
128- switch geoObj->Dict.get("error") {
129- | Some(e) => e->JSON.Decode.string
130- | None => None
131- }
132- | None => None
133- }
134- | None => None
135- }
136-}
137-
138-// Common response handling for Stadia API calls
139-let handleStadiaResponse = (
140- json: JSON.t,
141- ~parseFeature: JSON.t => option<'a>,
142- ~wrapResults: array<'a> => 'response,
143- ~emptyResponse: 'response,
144- ~serverError: string => 'response,
145-): 'response => {
146- switch json->JSON.Decode.object {
147- | Some(obj) => {
148- // Check for geocoding error
149- switch checkGeocodingError(obj) {
150- | Some(errMsg) => serverError(errMsg)
151- | None => {
152- // Parse features
153- let features = switch obj->Dict.get("features") {
154- | Some(f) =>
155- switch f->JSON.Decode.array {
156- | Some(arr) => arr
157- | None => []
158- }
159- | None => []
160- }
161-
162- let results = features->Array.filterMap(parseFeature)
163-
164- if results->Array.length == 0 {
165- emptyResponse
166- } else {
167- wrapResults(results)
168- }
169- }
170- }
171- }
172- | None => serverError("Invalid JSON response")
173- }
174-}
175-
176-// Public modules
177-
178-module Autocomplete = {
179- type layer = layer
180-
181- type params = {
182- text: string,
183- focusPoint?: (float, float),
184- boundaryRect?: (float, float, float, float),
185- boundaryCircle?: (float, float, float),
186- layers?: array<layer>,
187- sources?: array<string>,
188- boundaryCountry?: array<string>,
189- boundaryGid?: string,
190- size?: int,
191- lang?: string,
192- }
193-
194- type result = {
195- name: string,
196- gid: string,
197- layer: layer,
198- coarseLocation: string,
199- distance?: float,
200- }
201-
202- type response =
203- | Results(array<result>)
204- | Empty
205- | ServerError(string)
206-
207- let buildQueryString = (~apiKey: option<string>, params: params): string => {
208- let parts = []
209-
210- // Required text parameter
211- parts->Array.push(`text=${encodeURIComponent(params.text)}`)
212-
213- // Optional focus point
214- switch params.focusPoint {
215- | Some((lat, lon)) => {
216- parts->Array.push(`focus.point.lat=${Float.toString(lat)}`)
217- parts->Array.push(`focus.point.lon=${Float.toString(lon)}`)
218- }
219- | None => ()
220- }
221-
222- // Optional boundary rect
223- switch params.boundaryRect {
224- | Some((minLon, maxLon, minLat, maxLat)) => {
225- parts->Array.push(`boundary.rect.min_lon=${Float.toString(minLon)}`)
226- parts->Array.push(`boundary.rect.max_lon=${Float.toString(maxLon)}`)
227- parts->Array.push(`boundary.rect.min_lat=${Float.toString(minLat)}`)
228- parts->Array.push(`boundary.rect.max_lat=${Float.toString(maxLat)}`)
229- }
230- | None => ()
231- }
232-
233- // Optional boundary circle
234- switch params.boundaryCircle {
235- | Some((lat, lon, radius)) => {
236- parts->Array.push(`boundary.circle.lat=${Float.toString(lat)}`)
237- parts->Array.push(`boundary.circle.lon=${Float.toString(lon)}`)
238- parts->Array.push(`boundary.circle.radius=${Float.toString(radius)}`)
239- }
240- | None => ()
241- }
242-
243- // Optional layers
244- switch params.layers {
245- | Some(layers) if layers->Array.length > 0 => {
246- let layerStr = layers->Array.map(layerToString)->Array.join(",")
247- parts->Array.push(`layers=${encodeURIComponent(layerStr)}`)
248- }
249- | _ => ()
250- }
251-
252- // Optional sources
253- switch params.sources {
254- | Some(sources) if sources->Array.length > 0 => {
255- let sourceStr = sources->Array.join(",")
256- parts->Array.push(`sources=${encodeURIComponent(sourceStr)}`)
257- }
258- | _ => ()
259- }
260-
261- // Optional boundary country
262- switch params.boundaryCountry {
263- | Some(countries) if countries->Array.length > 0 => {
264- let countryStr = countries->Array.join(",")
265- parts->Array.push(`boundary.country=${encodeURIComponent(countryStr)}`)
266- }
267- | _ => ()
268- }
269-
270- // Optional boundary gid
271- switch params.boundaryGid {
272- | Some(gid) => parts->Array.push(`boundary.gid=${encodeURIComponent(gid)}`)
273- | None => ()
274- }
275-
276- // Optional size
277- switch params.size {
278- | Some(size) => parts->Array.push(`size=${Int.toString(size)}`)
279- | None => ()
280- }
281-
282- // Optional lang
283- switch params.lang {
284- | Some(lang) => parts->Array.push(`lang=${encodeURIComponent(lang)}`)
285- | None => ()
286- }
287-
288- // API key if provided
289- switch apiKey {
290- | Some(key) => parts->Array.push(`api_key=${encodeURIComponent(key)}`)
291- | None => ()
292- }
293-
294- parts->Array.join("&")
295- }
296-
297- let parseFeature = (feature: JSON.t): option<result> => {
298- switch feature->JSON.Decode.object {
299- | Some(obj) => {
300- let properties = switch obj->Dict.get("properties") {
301- | Some(p) => p->JSON.Decode.object
302- | None => None
303- }
304-
305- switch properties {
306- | Some(props) => {
307- let (name, gid, layer, coarseLocation) = parseBaseProperties(props)
308-
309- // Extract optional distance
310- let distance = switch props->Dict.get("distance") {
311- | Some(d) =>
312- switch d->JSON.Decode.float {
313- | Some(n) => Some(n)
314- | None => None
315- }
316- | None => None
317- }
318-
319- switch layer {
320- | Some(l) =>
321- Some({
322- name: name,
323- gid: gid,
324- layer: l,
325- coarseLocation: coarseLocation,
326- distance: ?distance,
327- })
328- | None => None
329- }
330- }
331- | None => None
332- }
333- }
334- | None => None
335- }
336- }
337-
338- let search = async (~params: params): response => {
339- let apiKey = getApiKey()
340- let queryString = buildQueryString(~apiKey, params)
341- let url = `https://api.stadiamaps.com/geocoding/v2/autocomplete?${queryString}`
342-
343- try {
344- let response = await Fetch.get(url)
345- ->Fetch.onResponse(
346- ~onSuccess=json => json,
347- ~onError=(status, message) => {
348- JSON.Encode.object(Dict.fromArray([
349- ("__error", JSON.Encode.bool(true)),
350- ("__status", JSON.Encode.float(Int.toFloat(status))),
351- ("__message", JSON.Encode.string(message)),
352- ]))
353- }
354- )
355- ->Fetch.send
356-
357- // Check if we got an error response from Fetch
358- switch response->JSON.Decode.object {
359- | Some(obj) =>
360- switch obj->Dict.get("__error") {
361- | Some(_) => {
362- let status = switch obj->Dict.get("__status") {
363- | Some(s) =>
364- switch s->JSON.Decode.float {
365- | Some(n) => n->Float.toInt->Int.toString
366- | None => "0"
367- }
368- | None => "0"
369- }
370- let message = switch obj->Dict.get("__message") {
371- | Some(m) =>
372- switch m->JSON.Decode.string {
373- | Some(s) => s
374- | None => "Unknown error"
375- }
376- | None => "Unknown error"
377- }
378- ServerError(`${status}: ${message}`)
379- }
380- | None => {
381- handleStadiaResponse(
382- response,
383- ~parseFeature=parseFeature,
384- ~wrapResults=results => Results(results),
385- ~emptyResponse=Empty,
386- ~serverError=msg => ServerError(msg),
387- )
388- }
389- }
390- | None => ServerError("Invalid JSON response")
391- }
392- } catch {
393- | _ => ServerError("Network error")
394- }
395- }
396-}
397-
398-// End of Autocomplete module
399-
400-module Place = {
401- type addressComponents = {
402- number?: string,
403- street?: string,
404- postal_code?: string,
405- }
406-
407- type whosonfirstEntity = {
408- gid: string,
409- name: string,
410- }
411-
412- type whosonfirstContext = {
413- country?: whosonfirstEntity,
414- region?: whosonfirstEntity,
415- county?: whosonfirstEntity,
416- locality?: whosonfirstEntity,
417- neighbourhood?: whosonfirstEntity,
418- }
419-
420- type context = {
421- whosonfirst?: whosonfirstContext,
422- iso_3166_a2?: string,
423- iso_3166_a3?: string,
424- }
425-
426- type params = {
427- gid: string,
428- lang?: string,
429- }
430-
431- type result = {
432- name: string,
433- gid: string,
434- layer: Autocomplete.layer,
435- coarseLocation: string,
436- coordinates: (float, float), // (lon, lat)
437- formattedAddressLine?: string,
438- formattedAddressLines?: array<string>,
439- addressComponents?: addressComponents,
440- context?: context,
441- }
442-
443- type response =
444- | Found(result)
445- | NotFound
446- | ServerError(string)
447-
448- let buildQueryString = (~apiKey: option<string>, params: params): string => {
449- let parts = []
450-
451- // Required ids parameter (single gid)
452- parts->Array.push(`ids=${encodeURIComponent(params.gid)}`)
453-
454- // Optional lang
455- switch params.lang {
456- | Some(lang) => parts->Array.push(`lang=${encodeURIComponent(lang)}`)
457- | None => ()
458- }
459-
460- // API key if provided
461- switch apiKey {
462- | Some(key) => parts->Array.push(`api_key=${encodeURIComponent(key)}`)
463- | None => ()
464- }
465-
466- parts->Array.join("&")
467- }
468-
469- let parseFeature = (feature: JSON.t): option<result> => {
470- switch feature->JSON.Decode.object {
471- | Some(obj) => {
472- let properties = switch obj->Dict.get("properties") {
473- | Some(p) => p->JSON.Decode.object
474- | None => None
475- }
476-
477- let geometry = switch obj->Dict.get("geometry") {
478- | Some(g) => g->JSON.Decode.object
479- | None => None
480- }
481-
482- switch (properties, geometry) {
483- | (Some(props), Some(geo)) => {
484- let (name, gid, layer, coarseLocation) = parseBaseProperties(props)
485-
486- // Parse geometry coordinates
487- let coordinates = switch geo->Dict.get("coordinates") {
488- | Some(c) =>
489- switch c->JSON.Decode.array {
490- | Some(arr) if arr->Array.length >= 2 => {
491- let lon = switch arr[0] {
492- | Some(lonJson) =>
493- switch lonJson->JSON.Decode.float {
494- | Some(f) => f
495- | None => 0.0
496- }
497- | None => 0.0
498- }
499- let lat = switch arr[1] {
500- | Some(latJson) =>
501- switch latJson->JSON.Decode.float {
502- | Some(f) => f
503- | None => 0.0
504- }
505- | None => 0.0
506- }
507- Some((lon, lat))
508- }
509- | _ => None
510- }
511- | None => None
512- }
513-
514- // Parse formatted addresses
515- let formattedAddressLine = switch props->Dict.get("formatted_address_line") {
516- | Some(v) => v->JSON.Decode.string
517- | None => None
518- }
519-
520- let formattedAddressLines = switch props->Dict.get("formatted_address_lines") {
521- | Some(v) =>
522- switch v->JSON.Decode.array {
523- | Some(arr) =>
524- arr->Array.filterMap(item => item->JSON.Decode.string)->Some
525- | None => None
526- }
527- | None => None
528- }
529-
530- // Parse address_components
531- let addressComponents = switch props->Dict.get("address_components") {
532- | Some(ac) =>
533- switch ac->JSON.Decode.object {
534- | Some(obj) => {
535- let number = obj->Dict.get("number")->Option.flatMap(JSON.Decode.string)
536- let street = obj->Dict.get("street")->Option.flatMap(JSON.Decode.string)
537- let postal_code = obj->Dict.get("postal_code")->Option.flatMap(JSON.Decode.string)
538- Some({
539- ?number,
540- ?street,
541- ?postal_code,
542- })
543- }
544- | None => None
545- }
546- | None => None
547- }
548-
549- // Parse context (whosonfirst and ISO codes)
550- let context = switch props->Dict.get("context") {
551- | Some(ctx) =>
552- switch ctx->JSON.Decode.object {
553- | Some(ctxObj) => {
554- let parseEntity = key => {
555- switch ctxObj->Dict.get(key) {
556- | Some(e) =>
557- switch e->JSON.Decode.object {
558- | Some(eObj) => {
559- let gidOpt = eObj->Dict.get("gid")->Option.flatMap(JSON.Decode.string)
560- let nameOpt = eObj->Dict.get("name")->Option.flatMap(JSON.Decode.string)
561- switch (gidOpt, nameOpt) {
562- | (Some(gid), Some(name)) => Some({gid, name})
563- | _ => None
564- }
565- }
566- | None => None
567- }
568- | None => None
569- }
570- }
571-
572- let whosonfirst = {
573- let country = parseEntity("country")
574- let region = parseEntity("region")
575- let county = parseEntity("county")
576- let locality = parseEntity("locality")
577- let neighbourhood = parseEntity("neighbourhood")
578- switch (country, region, county, locality, neighbourhood) {
579- | (None, None, None, None, None) => None
580- | _ =>
581- Some({
582- ?country,
583- ?region,
584- ?county,
585- ?locality,
586- ?neighbourhood,
587- })
588- }
589- }
590-
591- let iso_3166_a2 = ctxObj->Dict.get("iso_3166_a2")->Option.flatMap(JSON.Decode.string)
592- let iso_3166_a3 = ctxObj->Dict.get("iso_3166_a3")->Option.flatMap(JSON.Decode.string)
593-
594- switch (whosonfirst, iso_3166_a2, iso_3166_a3) {
595- | (None, None, None) => None
596- | _ =>
597- Some({
598- ?whosonfirst,
599- ?iso_3166_a2,
600- ?iso_3166_a3,
601- })
602- }
603- }
604- | None => None
605- }
606- | None => None
607- }
608-
609- switch (layer, coordinates) {
610- | (Some(l), Some(coords)) =>
611- Some({
612- name: name,
613- gid: gid,
614- layer: l,
615- coarseLocation: coarseLocation,
616- coordinates: coords,
617- ?formattedAddressLine,
618- ?formattedAddressLines,
619- ?addressComponents,
620- ?context,
621- })
622- | _ => None
623- }
624- }
625- | _ => None
626- }
627- }
628- | None => None
629- }
630- }
631-
632- let getDetails = async (~params: params): response => {
633- let apiKey = getApiKey()
634- let queryString = buildQueryString(~apiKey, params)
635- let url = `https://api.stadiamaps.com/geocoding/v2/place_details?${queryString}`
636-
637- try {
638- let response = await Fetch.get(url)
639- ->Fetch.onResponse(
640- ~onSuccess=json => json,
641- ~onError=(status, message) => {
642- JSON.Encode.object(Dict.fromArray([
643- ("__error", JSON.Encode.bool(true)),
644- ("__status", JSON.Encode.float(Int.toFloat(status))),
645- ("__message", JSON.Encode.string(message)),
646- ]))
647- }
648- )
649- ->Fetch.send
650-
651- // Check if we got an error response from Fetch
652- switch response->JSON.Decode.object {
653- | Some(obj) =>
654- switch obj->Dict.get("__error") {
655- | Some(_) => {
656- let status = switch obj->Dict.get("__status") {
657- | Some(s) =>
658- switch s->JSON.Decode.float {
659- | Some(n) => n->Float.toInt->Int.toString
660- | None => "0"
661- }
662- | None => "0"
663- }
664- let message = switch obj->Dict.get("__message") {
665- | Some(m) =>
666- switch m->JSON.Decode.string {
667- | Some(s) => s
668- | None => "Unknown error"
669- }
670- | None => "Unknown error"
671- }
672- ServerError(`${status}: ${message}`)
673- }
674- | None => {
675- // For Place details, we expect exactly one result
676- switch handleStadiaResponse(
677- response,
678- ~parseFeature=parseFeature,
679- ~wrapResults=results => {
680- switch results[0] {
681- | Some(result) => Found(result)
682- | None => NotFound
683- }
684- },
685- ~emptyResponse=NotFound,
686- ~serverError=msg => ServerError(msg),
687- ) {
688- | Found(_) as found => found
689- | NotFound as notFound => notFound
690- | ServerError(_) as err => err
691- }
692- }
693- }
694- | None => ServerError("Invalid JSON response")
695- }
696- } catch {
697- | _ => ServerError("Network error")
698- }
699- }
700-}
+0,
-100
1@@ -1,100 +0,0 @@
2-// Public interface for Stadia Maps API bindings
3-// Internal implementation details are hidden
4-
5-module Autocomplete: {
6- type layer = [
7- | #poi
8- | #address
9- | #street
10- | #country
11- | #macroregion
12- | #region
13- | #macrocounty
14- | #county
15- | #locality
16- | #localadmin
17- | #borough
18- | #neighbourhood
19- | #postalcode
20- | #coarse
21- ]
22-
23- type params = {
24- text: string,
25- focusPoint?: (float, float),
26- boundaryRect?: (float, float, float, float),
27- boundaryCircle?: (float, float, float),
28- layers?: array<layer>,
29- sources?: array<string>,
30- boundaryCountry?: array<string>,
31- boundaryGid?: string,
32- size?: int,
33- lang?: string,
34- }
35-
36- type result = {
37- name: string,
38- gid: string,
39- layer: layer,
40- coarseLocation: string,
41- distance?: float,
42- }
43-
44- type response =
45- | Results(array<result>)
46- | Empty
47- | ServerError(string)
48-
49- let search: (~params: params) => promise<response>
50-}
51-
52-module Place: {
53- type addressComponents = {
54- number?: string,
55- street?: string,
56- postal_code?: string,
57- }
58-
59- type whosonfirstEntity = {
60- gid: string,
61- name: string,
62- }
63-
64- type whosonfirstContext = {
65- country?: whosonfirstEntity,
66- region?: whosonfirstEntity,
67- county?: whosonfirstEntity,
68- locality?: whosonfirstEntity,
69- neighbourhood?: whosonfirstEntity,
70- }
71-
72- type context = {
73- whosonfirst?: whosonfirstContext,
74- iso_3166_a2?: string,
75- iso_3166_a3?: string,
76- }
77-
78- type params = {
79- gid: string,
80- lang?: string,
81- }
82-
83- type result = {
84- name: string,
85- gid: string,
86- layer: Autocomplete.layer,
87- coarseLocation: string,
88- coordinates: (float, float), // (lon, lat)
89- formattedAddressLine?: string,
90- formattedAddressLines?: array<string>,
91- addressComponents?: addressComponents,
92- context?: context,
93- }
94-
95- type response =
96- | Found(result)
97- | NotFound
98- | ServerError(string)
99-
100- let getDetails: (~params: params) => promise<response>
101-}
+101,
-81
1@@ -44,34 +44,64 @@ type response =
2 let decodeResult: JSON.t => option<result> = json => {
3 switch json->JSON.Decode.object {
4 | Some(dict) => {
5- let gid = dict->Dict.get("gid")->Option.flatMap(JSON.Decode.float)->Option.map(f => f->Float.toInt)
6+ let gid =
7+ dict->Dict.get("gid")->Option.flatMap(JSON.Decode.float)->Option.map(f => f->Float.toInt)
8 let name = dict->Dict.get("name")->Option.flatMap(JSON.Decode.string)
9 let placetype = dict->Dict.get("placetype")->Option.flatMap(JSON.Decode.string)
10 let lon = dict->Dict.get("lon")->Option.flatMap(JSON.Decode.float)
11 let lat = dict->Dict.get("lat")->Option.flatMap(JSON.Decode.float)
12 let distance = dict->Dict.get("distance")->Option.flatMap(JSON.Decode.float)
13 let bearing = dict->Dict.get("bearing")->Option.flatMap(JSON.Decode.float)
14- let region = dict
15+ let region =
16+ dict
17 ->Dict.get("region")
18 ->Option.flatMap(JSON.Decode.array)
19 ->Option.map(arr => arr->Array.filterMap(JSON.Decode.string))
20 let country = dict->Dict.get("country")->Option.flatMap(JSON.Decode.string)
21
22 switch (gid, name, placetype, lon, lat, distance, bearing, region, country) {
23- | (Some(gid), Some(name), Some(placetype), Some(lon), Some(lat), Some(distance), Some(bearing), Some(region), Some(country)) =>
24+ | (
25+ Some(gid),
26+ Some(name),
27+ Some(placetype),
28+ Some(lon),
29+ Some(lat),
30+ Some(distance),
31+ Some(bearing),
32+ Some(region),
33+ Some(country),
34+ ) =>
35 Some({gid, name, placetype, lon, lat, distance, bearing, region, country})
36 | _ => {
37 // DEBUG: Log which fields are missing
38 let missing = []
39- if gid->Option.isNone { missing->Array.push("gid") }
40- if name->Option.isNone { missing->Array.push("name") }
41- if placetype->Option.isNone { missing->Array.push("placetype") }
42- if lon->Option.isNone { missing->Array.push("lon") }
43- if lat->Option.isNone { missing->Array.push("lat") }
44- if distance->Option.isNone { missing->Array.push("distance") }
45- if bearing->Option.isNone { missing->Array.push("bearing") }
46- if region->Option.isNone { missing->Array.push("region") }
47- if country->Option.isNone { missing->Array.push("country") }
48+ if gid->Option.isNone {
49+ missing->Array.push("gid")
50+ }
51+ if name->Option.isNone {
52+ missing->Array.push("name")
53+ }
54+ if placetype->Option.isNone {
55+ missing->Array.push("placetype")
56+ }
57+ if lon->Option.isNone {
58+ missing->Array.push("lon")
59+ }
60+ if lat->Option.isNone {
61+ missing->Array.push("lat")
62+ }
63+ if distance->Option.isNone {
64+ missing->Array.push("distance")
65+ }
66+ if bearing->Option.isNone {
67+ missing->Array.push("bearing")
68+ }
69+ if region->Option.isNone {
70+ missing->Array.push("region")
71+ }
72+ if country->Option.isNone {
73+ missing->Array.push("country")
74+ }
75 if missing->Array.length > 0 {
76 Console.error(`[Waldo] decodeResult: Missing fields: ${missing->Array.join(", ")}`)
77 }
78@@ -92,26 +122,30 @@ let decodeResult: JSON.t => option<result> = json => {
79 let decodeGeocodeResponse: JSON.t => option<geocodeResponse> = json => {
80 switch json->JSON.Decode.object {
81 | Some(dict) => {
82- Console.log("[Waldo] decodeGeocodeResponse: Got JSON object, extracting fields...")
83- let results = dict
84+ let results =
85+ dict
86 ->Dict.get("results")
87 ->Option.flatMap(JSON.Decode.array)
88 ->Option.map(arr => arr->Array.filterMap(decodeResult))
89- let numResults = dict
90+ let numResults =
91+ dict
92 ->Dict.get("num_results")
93 ->Option.flatMap(JSON.Decode.float)
94 ->Option.map(f => f->Float.toInt)
95
96 switch (results, numResults) {
97- | (Some(results), Some(numResults)) => {
98- Console.log(`[Waldo] decodeGeocodeResponse: Success with ${numResults->Int.toString} results`)
99- Some({results, numResults})
100- }
101+ | (Some(results), Some(numResults)) => Some({results, numResults})
102 | _ => {
103 let missing = []
104- if results->Option.isNone { missing->Array.push("results") }
105- if numResults->Option.isNone { missing->Array.push("num_results") }
106- Console.error(`[Waldo] decodeGeocodeResponse: Missing fields: ${missing->Array.join(", ")}`)
107+ if results->Option.isNone {
108+ missing->Array.push("results")
109+ }
110+ if numResults->Option.isNone {
111+ missing->Array.push("num_results")
112+ }
113+ Console.error(
114+ `[Waldo] decodeGeocodeResponse: Missing fields: ${missing->Array.join(", ")}`,
115+ )
116 None
117 }
118 }
119@@ -128,11 +162,9 @@ let decodeGeocodeResponse: JSON.t => option<geocodeResponse> = json => {
120 */
121 let decodeErrorResponse: JSON.t => option<errorResponse> = json => {
122 switch json->JSON.Decode.object {
123- | Some(dict) => {
124- switch dict->Dict.get("error")->Option.flatMap(JSON.Decode.string) {
125- | Some(error) => Some({error: error})
126- | None => None
127- }
128+ | Some(dict) => switch dict->Dict.get("error")->Option.flatMap(JSON.Decode.string) {
129+ | Some(error) => Some({error: error})
130+ | None => None
131 }
132 | None => None
133 }
134@@ -163,22 +195,20 @@ module Forward = {
135 let url = buildUrl(params)
136
137 try {
138- Console.log("[Waldo] Forward: Starting fetch request...")
139 let response = await Fetch.get(url)
140- ->Fetch.onResponse(
141- ~onSuccess=json => json,
142- ~onError=(status, message) => {
143- JSON.Encode.object(Dict.fromArray([
144+ ->Fetch.onResponse(
145+ ~onSuccess=json => json,
146+ ~onError=(status, message) => {
147+ JSON.Encode.object(
148+ Dict.fromArray([
149 ("__error", JSON.Encode.bool(true)),
150 ("__status", JSON.Encode.float(Int.toFloat(status))),
151 ("__message", JSON.Encode.string(message)),
152- ]))
153- }
154- )
155- ->Fetch.send
156-
157- // DEBUG: Log raw response
158- Console.log("[Waldo] Forward: Raw response received")
159+ ]),
160+ )
161+ },
162+ )
163+ ->Fetch.send
164
165 // DEBUG: Log the actual response content
166 let responseStr = try {
167@@ -186,12 +216,11 @@ module Forward = {
168 } catch {
169 | _ => "[Failed to stringify response]"
170 }
171- Console.log(`[Waldo] Forward: Response content: ${responseStr}`)
172+ Log.debug(`[Waldo] Forward: Response content: ${responseStr}`)
173
174 // Check if we got an error response from Fetch
175 switch response->JSON.Decode.object {
176 | Some(obj) =>
177- Console.log("[Waldo] Forward: Response is JSON object")
178 switch obj->Dict.get("__error") {
179 | Some(_) => {
180 let status = switch obj->Dict.get("__status") {
181@@ -213,27 +242,20 @@ module Forward = {
182 Console.error(`[Waldo] Forward: Fetch error: ${status} - ${message}`)
183 Error(`${status}: ${message}`)
184 }
185- | None => {
186- // Try to decode as success response
187- Console.log("[Waldo] Forward: No __error field found, attempting to decode geocode response...")
188- Console.log(`[Waldo] Forward: Response object keys: ${obj->Dict.keysToArray->Array.join(", ")}`)
189- switch decodeGeocodeResponse(response) {
190- | Some(resp) => {
191- Console.log(`[Waldo] Forward: Decoded ${resp.numResults->Int.toString} results`)
192- Success(resp.results)
193- }
194- | None => {
195- Console.error("[Waldo] Forward: Failed to decode geocode response")
196- // Try to decode as error response
197- switch decodeErrorResponse(response) {
198- | Some(err) => {
199- Console.error(`[Waldo] Forward: API error: ${err.error}`)
200- Error(err.error)
201- }
202- | None => {
203- Console.error("[Waldo] Forward: Unknown response format")
204- Error("Invalid response format")
205- }
206+ | None => // Try to decode as success response
207+ switch decodeGeocodeResponse(response) {
208+ | Some(resp) => Success(resp.results)
209+ | None => {
210+ Console.error("[Waldo] Forward: Failed to decode geocode response")
211+ // Try to decode as error response
212+ switch decodeErrorResponse(response) {
213+ | Some(err) => {
214+ Console.error(`[Waldo] Forward: API error: ${err.error}`)
215+ Error(err.error)
216+ }
217+ | None => {
218+ Console.error("[Waldo] Forward: Unknown response format")
219+ Error("Invalid response format")
220 }
221 }
222 }
223@@ -270,17 +292,19 @@ module Reverse = {
224
225 try {
226 let response = await Fetch.get(url)
227- ->Fetch.onResponse(
228- ~onSuccess=json => json,
229- ~onError=(status, message) => {
230- JSON.Encode.object(Dict.fromArray([
231+ ->Fetch.onResponse(
232+ ~onSuccess=json => json,
233+ ~onError=(status, message) => {
234+ JSON.Encode.object(
235+ Dict.fromArray([
236 ("__error", JSON.Encode.bool(true)),
237 ("__status", JSON.Encode.float(Int.toFloat(status))),
238 ("__message", JSON.Encode.string(message)),
239- ]))
240- }
241- )
242- ->Fetch.send
243+ ]),
244+ )
245+ },
246+ )
247+ ->Fetch.send
248
249 // Check if we got an error response from Fetch
250 switch response->JSON.Decode.object {
251@@ -305,17 +329,13 @@ module Reverse = {
252 }
253 Error(`${status}: ${message}`)
254 }
255- | None => {
256- // Try to decode as success response
257- switch decodeGeocodeResponse(response) {
258- | Some(resp) => Success(resp.results)
259- | None => {
260- // Try to decode as error response
261- switch decodeErrorResponse(response) {
262- | Some(err) => Error(err.error)
263- | None => Error("Invalid response format")
264- }
265- }
266+ | None => // Try to decode as success response
267+ switch decodeGeocodeResponse(response) {
268+ | Some(resp) => Success(resp.results)
269+ | None => // Try to decode as error response
270+ switch decodeErrorResponse(response) {
271+ | Some(err) => Error(err.error)
272+ | None => Error("Invalid response format")
273 }
274 }
275 }
+0,
-0
+0,
-2
1@@ -1,6 +1,5 @@
2 // src/ui/Editor.res
3 // Preact-compatible FFI bindings for @tiptap/react composable API
4-@module("./content.js") @val external content: RescriptCore.JSON.t = "content"
5
6 // Re-export editor type for idiomatic Rescript usage
7 type t = Tiptap.t
8@@ -124,7 +123,6 @@ let make = (
9 | None => ()
10 }
11 },
12- content,
13 })
14
15 // Store reference for onUpdate callback
+0,
-19
1@@ -1,19 +0,0 @@
2-export const content = {
3- type: "doc",
4- content: [
5- {
6- type: "heading",
7- attrs: { level: 1 },
8- content: [{ type: "text", text: "Start with a title..." }],
9- },
10- {
11- type: "paragraph",
12- content: [
13- {
14- type: "text",
15- text: "To add a waypoint type @ or click on the menu button at the start of an empty line...",
16- },
17- ],
18- },
19- ],
20-};
+0,
-586
1@@ -1,586 +0,0 @@
2-open VitestHelpers
3-
4-@module("vitest") @val external vi: 'a = "vi"
5-
6-let mockFetchJson: (JSON.t, int, string) => unit = %raw(`
7- function(json, status, statusText) {
8- globalThis.fetch = vi.fn(() => Promise.resolve({
9- ok: status >= 200 && status < 300,
10- status: status,
11- statusText: statusText,
12- json: () => Promise.resolve(json)
13- }));
14- }
15-`)
16-
17-let setupMockWithCapture: unit => 'a = %raw(`
18- function() {
19- const calls = [];
20- globalThis.fetch = vi.fn((url, init) => {
21- calls.push({url, init});
22- return Promise.resolve({
23- ok: true,
24- status: 200,
25- statusText: 'OK',
26- json: () => Promise.resolve({
27- type: 'FeatureCollection',
28- features: [],
29- geocoding: { attribution: 'test' }
30- })
31- });
32- });
33- return calls;
34- }
35-`)
36-
37-let getUrl: 'a => string = %raw(`
38- function(calls) {
39- return calls[0].url;
40- }
41-`)
42-
43-describe("Stadia.Autocomplete", () => {
44- testAsync("returns Results with parsed data for successful search", async t => {
45- let responseJson = JSON.Encode.object(Dict.fromArray([
46- ("type", JSON.Encode.string("FeatureCollection")),
47- ("geocoding", JSON.Encode.object(Dict.fromArray([
48- ("attribution", JSON.Encode.string("https://stadiamaps.com/attribution/")),
49- ("query", JSON.Encode.object(Dict.fromArray([("text", JSON.Encode.string("Põhja pst 27"))])))
50- ]))),
51- ("features", JSON.Encode.array([
52- JSON.Encode.object(Dict.fromArray([
53- ("type", JSON.Encode.string("Feature")),
54- ("geometry", JSON.Encode.null),
55- ("properties", JSON.Encode.object(Dict.fromArray([
56- ("name", JSON.Encode.string("Põhja Puiestee 27")),
57- ("gid", JSON.Encode.string("openaddresses:address:ee/countrywide-addresses-country:5cbc00647aa5c976")),
58- ("layer", JSON.Encode.string("address")),
59- ("coarse_location", JSON.Encode.string("Tallinn, Harju, Estonia")),
60- ("precision", JSON.Encode.string("point"))
61- ])))
62- ])),
63- JSON.Encode.object(Dict.fromArray([
64- ("type", JSON.Encode.string("Feature")),
65- ("geometry", JSON.Encode.null),
66- ("properties", JSON.Encode.object(Dict.fromArray([
67- ("name", JSON.Encode.string("Põhja pst 27")),
68- ("gid", JSON.Encode.string("openstreetmap:address:way/224094258")),
69- ("layer", JSON.Encode.string("address")),
70- ("coarse_location", JSON.Encode.string("Viljandi, Estonia")),
71- ("precision", JSON.Encode.string("centroid"))
72- ])))
73- ]))
74- ]))
75- ]))
76- mockFetchJson(responseJson, 200, "OK")
77-
78- let result = await Stadia.Autocomplete.search(~params={
79- text: "Põhja pst 27"
80- })
81-
82- switch result {
83- | Results(results) => {
84- t->expect(results->Array.length)->toEqual(2)
85-
86- switch results[0] {
87- | Some(first) => {
88- t->expect(first.name)->toEqual("Põhja Puiestee 27")
89- t->expect(first.gid)->toEqual("openaddresses:address:ee/countrywide-addresses-country:5cbc00647aa5c976")
90- t->expect(first.layer)->toEqual(#address)
91- t->expect(first.coarseLocation)->toEqual("Tallinn, Harju, Estonia")
92- t->expect(first.distance)->toEqual(None)
93- }
94- | None => t->expect(true)->toEqual(false)
95- }
96-
97- switch results[1] {
98- | Some(second) => {
99- t->expect(second.name)->toEqual("Põhja pst 27")
100- t->expect(second.layer)->toEqual(#address)
101- }
102- | None => t->expect(true)->toEqual(false)
103- }
104- }
105- | _ => t->expect(true)->toEqual(false) // Should be Results
106- }
107- })
108-
109- testAsync("returns Results with distance when provided", async t => {
110- let responseJson = JSON.Encode.object(Dict.fromArray([
111- ("type", JSON.Encode.string("FeatureCollection")),
112- ("geocoding", JSON.Encode.object(Dict.fromArray([
113- ("attribution", JSON.Encode.string("test"))
114- ]))),
115- ("features", JSON.Encode.array([
116- JSON.Encode.object(Dict.fromArray([
117- ("type", JSON.Encode.string("Feature")),
118- ("geometry", JSON.Encode.null),
119- ("properties", JSON.Encode.object(Dict.fromArray([
120- ("name", JSON.Encode.string("Test Location")),
121- ("gid", JSON.Encode.string("test:123")),
122- ("layer", JSON.Encode.string("address")),
123- ("coarse_location", JSON.Encode.string("City, Country")),
124- ("distance", JSON.Encode.float(1.5))
125- ])))
126- ]))
127- ]))
128- ]))
129- mockFetchJson(responseJson, 200, "OK")
130-
131- let result = await Stadia.Autocomplete.search(~params={
132- text: "test",
133- focusPoint: (59.437, 24.753)
134- })
135-
136- switch result {
137- | Results(results) => {
138- t->expect(results->Array.length)->toEqual(1)
139- switch results[0] {
140- | Some(first) => t->expect(first.distance)->toEqual(Some(1.5))
141- | None => t->expect(true)->toEqual(false)
142- }
143- }
144- | _ => t->expect(true)->toEqual(false)
145- }
146- })
147-
148- testAsync("returns Empty for empty features array", async t => {
149- let responseJson = JSON.Encode.object(Dict.fromArray([
150- ("type", JSON.Encode.string("FeatureCollection")),
151- ("geocoding", JSON.Encode.object(Dict.fromArray([
152- ("attribution", JSON.Encode.string("test"))
153- ]))),
154- ("features", JSON.Encode.array([]))
155- ]))
156- mockFetchJson(responseJson, 200, "OK")
157-
158- let result = await Stadia.Autocomplete.search(~params={
159- text: "xyznonexistent12345"
160- })
161-
162- t->expect(result)->toEqual(Stadia.Autocomplete.Empty)
163- })
164-
165- testAsync("returns ServerError on HTTP error", async t => {
166- let errorJson = JSON.Encode.object(Dict.fromArray([
167- ("error", JSON.Encode.string("Invalid API key"))
168- ]))
169- mockFetchJson(errorJson, 401, "Unauthorized")
170-
171- let result = await Stadia.Autocomplete.search(~params={
172- text: "test"
173- })
174-
175- switch result {
176- | ServerError(msg) => t->expect(msg)->toEqual("401: Invalid API key")
177- | _ => t->expect(true)->toEqual(false)
178- }
179- })
180-
181- testAsync("returns ServerError on geocoding error", async t => {
182- let responseJson = JSON.Encode.object(Dict.fromArray([
183- ("type", JSON.Encode.string("FeatureCollection")),
184- ("geocoding", JSON.Encode.object(Dict.fromArray([
185- ("attribution", JSON.Encode.string("test")),
186- ("error", JSON.Encode.string("Invalid parameter: text is required"))
187- ]))),
188- ("features", JSON.Encode.array([]))
189- ]))
190- mockFetchJson(responseJson, 200, "OK")
191-
192- let result = await Stadia.Autocomplete.search(~params={
193- text: "test"
194- })
195-
196- switch result {
197- | ServerError(msg) => t->expect(msg)->toEqual("Invalid parameter: text is required")
198- | _ => t->expect(true)->toEqual(false)
199- }
200- })
201-
202- testAsync("encodes text parameter in query string", async t => {
203- let _calls = setupMockWithCapture()
204-
205- let _result = await Stadia.Autocomplete.search(~params={
206- text: "Põhja pst 27"
207- })
208-
209- let url = getUrl(_calls)
210- t->expect(url->String.includes("text=P%C3%B5hja%20pst%2027"))->toEqual(true)
211- })
212-
213- testAsync("encodes focus point parameters", async t => {
214- let _calls = setupMockWithCapture()
215-
216- let _result = await Stadia.Autocomplete.search(~params={
217- text: "test",
218- focusPoint: (59.437, 24.753)
219- })
220-
221- let url = getUrl(_calls)
222- t->expect(url->String.includes("focus.point.lat=59.437"))->toEqual(true)
223- t->expect(url->String.includes("focus.point.lon=24.753"))->toEqual(true)
224- })
225-
226- testAsync("encodes boundary rect parameters", async t => {
227- let _calls = setupMockWithCapture()
228-
229- let _result = await Stadia.Autocomplete.search(~params={
230- text: "test",
231- boundaryRect: (139.2794, 140.1471, 35.53308, 35.81346)
232- })
233-
234- let url = getUrl(_calls)
235- t->expect(url->String.includes("boundary.rect.min_lon=139.2794"))->toEqual(true)
236- t->expect(url->String.includes("boundary.rect.max_lon=140.1471"))->toEqual(true)
237- t->expect(url->String.includes("boundary.rect.min_lat=35.53308"))->toEqual(true)
238- t->expect(url->String.includes("boundary.rect.max_lat=35.81346"))->toEqual(true)
239- })
240-
241- testAsync("encodes boundary circle parameters", async t => {
242- let _calls = setupMockWithCapture()
243-
244- let _result = await Stadia.Autocomplete.search(~params={
245- text: "test",
246- boundaryCircle: (43.818156, -79.186484, 35.0)
247- })
248-
249- let url = getUrl(_calls)
250- t->expect(url->String.includes("boundary.circle.lat=43.818156"))->toEqual(true)
251- t->expect(url->String.includes("boundary.circle.lon=-79.186484"))->toEqual(true)
252- t->expect(url->String.includes("boundary.circle.radius=35"))->toEqual(true)
253- })
254-
255- testAsync("encodes layers as comma-separated", async t => {
256- let _calls = setupMockWithCapture()
257-
258- let _result = await Stadia.Autocomplete.search(~params={
259- text: "test",
260- layers: [#address, #poi, #coarse]
261- })
262-
263- let url = getUrl(_calls)
264- t->expect(url->String.includes("layers=address%2Cpoi%2Ccoarse") || url->String.includes("layers=address,poi,coarse"))->toEqual(true)
265- })
266-
267- testAsync("encodes sources as comma-separated", async t => {
268- let _calls = setupMockWithCapture()
269-
270- let _result = await Stadia.Autocomplete.search(~params={
271- text: "test",
272- sources: ["openstreetmap", "openaddresses"]
273- })
274-
275- let url = getUrl(_calls)
276- t->expect(url->String.includes("sources=openstreetmap%2Copenaddresses") || url->String.includes("sources=openstreetmap,openaddresses"))->toEqual(true)
277- })
278-
279- testAsync("encodes boundary country as comma-separated", async t => {
280- let _calls = setupMockWithCapture()
281-
282- let _result = await Stadia.Autocomplete.search(~params={
283- text: "test",
284- boundaryCountry: ["GBR", "FRA"]
285- })
286-
287- let url = getUrl(_calls)
288- t->expect(url->String.includes("boundary.country=GBR%2CFRA") || url->String.includes("boundary.country=GBR,FRA"))->toEqual(true)
289- })
290-
291- testAsync("encodes boundary gid", async t => {
292- let _calls = setupMockWithCapture()
293-
294- let _result = await Stadia.Autocomplete.search(~params={
295- text: "test",
296- boundaryGid: "whosonfirst:locality:101748355"
297- })
298-
299- let url = getUrl(_calls)
300- t->expect(url->String.includes("boundary.gid=whosonfirst%3Alocality%3A101748355"))->toEqual(true)
301- })
302-
303- testAsync("encodes size parameter", async t => {
304- let _calls = setupMockWithCapture()
305-
306- let _result = await Stadia.Autocomplete.search(~params={
307- text: "test",
308- size: 5
309- })
310-
311- let url = getUrl(_calls)
312- t->expect(url->String.includes("size=5"))->toEqual(true)
313- })
314-
315- testAsync("encodes lang parameter", async t => {
316- let _calls = setupMockWithCapture()
317-
318- let _result = await Stadia.Autocomplete.search(~params={
319- text: "test",
320- lang: "en"
321- })
322-
323- let url = getUrl(_calls)
324- t->expect(url->String.includes("lang=en"))->toEqual(true)
325- })
326-
327- testAsync("includes api_key in URL when env var is present", async t => {
328- // This test documents that the API key from VITE_STADIA_KEY will be included
329- // We cannot easily mock import.meta.env in vitest, but we verify the URL construction
330- // logic works by checking the actual URL format via the capture mechanism
331- // The implementation reads from import.meta.env.VITE_STADIA_KEY
332-
333- // For this test, we simply verify that if an API key were present,
334- // the query string construction would handle it properly
335- // (We test query string construction in other tests)
336-
337- let _calls = setupMockWithCapture()
338-
339- let _result = await Stadia.Autocomplete.search(~params={
340- text: "test"
341- })
342-
343- // Verify the URL was constructed and fetch was called
344- let url = getUrl(_calls)
345- t->expect(url->String.length > 0)->toEqual(true)
346- t->expect(url->String.includes("https://api.stadiamaps.com/geocoding/v2/autocomplete"))->toEqual(true)
347- })
348-
349- testAsync("handles all layer types correctly", async t => {
350- let testCases = [
351- ("poi", #poi),
352- ("address", #address),
353- ("street", #street),
354- ("country", #country),
355- ("macroregion", #macroregion),
356- ("region", #region),
357- ("macrocounty", #macrocounty),
358- ("county", #county),
359- ("locality", #locality),
360- ("localadmin", #localadmin),
361- ("borough", #borough),
362- ("neighbourhood", #neighbourhood),
363- ("postalcode", #postalcode),
364- ("coarse", #coarse),
365- ]
366-
367- let features = testCases->Array.mapWithIndex(((layerStr, _layerVariant), idx) => {
368- JSON.Encode.object(Dict.fromArray([
369- ("type", JSON.Encode.string("Feature")),
370- ("geometry", JSON.Encode.null),
371- ("properties", JSON.Encode.object(Dict.fromArray([
372- ("name", JSON.Encode.string(`Test ${idx->Int.toString}`)),
373- ("gid", JSON.Encode.string(`test:${idx->Int.toString}`)),
374- ("layer", JSON.Encode.string(layerStr)),
375- ("coarse_location", JSON.Encode.string("Test City"))
376- ])))
377- ]))
378- })
379-
380- let responseJson = JSON.Encode.object(Dict.fromArray([
381- ("type", JSON.Encode.string("FeatureCollection")),
382- ("geocoding", JSON.Encode.object(Dict.fromArray([
383- ("attribution", JSON.Encode.string("test"))
384- ]))),
385- ("features", JSON.Encode.array(features))
386- ]))
387- mockFetchJson(responseJson, 200, "OK")
388-
389- let result = await Stadia.Autocomplete.search(~params={
390- text: "test"
391- })
392-
393- switch result {
394- | Results(results) => {
395- t->expect(results->Array.length)->toEqual(testCases->Array.length)
396- testCases->Array.forEachWithIndex(((_, expectedLayer), idx) => {
397- t->expect(results[idx]->Option.map(r => r.layer))->toEqual(Some(expectedLayer))
398- })
399- }
400- | _ => t->expect(true)->toEqual(false)
401- }
402- })
403-})
404-
405-describe("Stadia.Place", () => {
406- testAsync("returns Found with coordinates for valid gid", async t => {
407- let responseJson = JSON.Encode.object(Dict.fromArray([
408- ("type", JSON.Encode.string("FeatureCollection")),
409- ("geocoding", JSON.Encode.object(Dict.fromArray([
410- ("attribution", JSON.Encode.string("https://stadiamaps.com/attribution/"))
411- ]))),
412- ("features", JSON.Encode.array([
413- JSON.Encode.object(Dict.fromArray([
414- ("type", JSON.Encode.string("Feature")),
415- ("geometry", JSON.Encode.object(Dict.fromArray([
416- ("type", JSON.Encode.string("Point")),
417- ("coordinates", JSON.Encode.array([
418- JSON.Encode.float(24.753),
419- JSON.Encode.float(59.437)
420- ]))
421- ]))),
422- ("properties", JSON.Encode.object(Dict.fromArray([
423- ("name", JSON.Encode.string("Põhja Puiestee 27")),
424- ("gid", JSON.Encode.string("openaddresses:address:ee/countrywide-addresses-country:5cbc00647aa5c976")),
425- ("layer", JSON.Encode.string("address")),
426- ("coarse_location", JSON.Encode.string("Tallinn, Harju, Estonia"))
427- ])))
428- ]))
429- ]))
430- ]))
431- mockFetchJson(responseJson, 200, "OK")
432-
433- let result = await Stadia.Place.getDetails(~params={
434- gid: "openaddresses:address:ee/countrywide-addresses-country:5cbc00647aa5c976"
435- })
436-
437- switch result {
438- | Found(place) => {
439- t->expect(place.name)->toEqual("Põhja Puiestee 27")
440- t->expect(place.gid)->toEqual("openaddresses:address:ee/countrywide-addresses-country:5cbc00647aa5c976")
441- t->expect(place.layer)->toEqual(#address)
442- t->expect(place.coarseLocation)->toEqual("Tallinn, Harju, Estonia")
443- let (lon, lat) = place.coordinates
444- t->expect(lon)->toEqual(24.753)
445- t->expect(lat)->toEqual(59.437)
446- }
447- | _ => t->expect(true)->toEqual(false) // Should be Found
448- }
449- })
450-
451- testAsync("returns NotFound for empty features", async t => {
452- let responseJson = JSON.Encode.object(Dict.fromArray([
453- ("type", JSON.Encode.string("FeatureCollection")),
454- ("geocoding", JSON.Encode.object(Dict.fromArray([
455- ("attribution", JSON.Encode.string("test"))
456- ]))),
457- ("features", JSON.Encode.array([]))
458- ]))
459- mockFetchJson(responseJson, 200, "OK")
460-
461- let result = await Stadia.Place.getDetails(~params={
462- gid: "nonexistent:123"
463- })
464-
465- t->expect(result)->toEqual(Stadia.Place.NotFound)
466- })
467-
468- testAsync("returns ServerError on HTTP error", async t => {
469- let errorJson = JSON.Encode.object(Dict.fromArray([
470- ("error", JSON.Encode.string("Not found"))
471- ]))
472- mockFetchJson(errorJson, 404, "Not Found")
473-
474- let result = await Stadia.Place.getDetails(~params={
475- gid: "test:123"
476- })
477-
478- switch result {
479- | ServerError(msg) => t->expect(msg)->toEqual("404: Not found")
480- | _ => t->expect(true)->toEqual(false)
481- }
482- })
483-
484- testAsync("returns ServerError on geocoding error", async t => {
485- let responseJson = JSON.Encode.object(Dict.fromArray([
486- ("type", JSON.Encode.string("FeatureCollection")),
487- ("geocoding", JSON.Encode.object(Dict.fromArray([
488- ("attribution", JSON.Encode.string("test")),
489- ("error", JSON.Encode.string("Invalid GID format"))
490- ]))),
491- ("features", JSON.Encode.array([]))
492- ]))
493- mockFetchJson(responseJson, 200, "OK")
494-
495- let result = await Stadia.Place.getDetails(~params={
496- gid: "invalid"
497- })
498-
499- switch result {
500- | ServerError(msg) => t->expect(msg)->toEqual("Invalid GID format")
501- | _ => t->expect(true)->toEqual(false)
502- }
503- })
504-
505- testAsync("encodes gid in ids parameter", async t => {
506- let _calls = setupMockWithCapture()
507-
508- let _result = await Stadia.Place.getDetails(~params={
509- gid: "openstreetmap:address:way/123"
510- })
511-
512- let url = getUrl(_calls)
513- t->expect(url->String.includes("ids=openstreetmap%3Aaddress%3Away%2F123"))->toEqual(true)
514- })
515-
516- testAsync("encodes lang parameter", async t => {
517- let _calls = setupMockWithCapture()
518-
519- let _result = await Stadia.Place.getDetails(~params={
520- gid: "test:123",
521- lang: "en"
522- })
523-
524- let url = getUrl(_calls)
525- t->expect(url->String.includes("lang=en"))->toEqual(true)
526- })
527-
528- testAsync("handles all layer types", async t => {
529- let testCases = [
530- ("poi", #poi),
531- ("address", #address),
532- ("street", #street),
533- ("country", #country),
534- ("macroregion", #macroregion),
535- ("region", #region),
536- ("macrocounty", #macrocounty),
537- ("county", #county),
538- ("locality", #locality),
539- ("localadmin", #localadmin),
540- ("borough", #borough),
541- ("neighbourhood", #neighbourhood),
542- ("postalcode", #postalcode),
543- ("coarse", #coarse),
544- ]
545-
546- let features = testCases->Array.mapWithIndex(((layerStr, _layerVariant), idx) => {
547- JSON.Encode.object(Dict.fromArray([
548- ("type", JSON.Encode.string("Feature")),
549- ("geometry", JSON.Encode.object(Dict.fromArray([
550- ("type", JSON.Encode.string("Point")),
551- ("coordinates", JSON.Encode.array([
552- JSON.Encode.float(idx->Int.toFloat),
553- JSON.Encode.float(idx->Int.toFloat)
554- ]))
555- ]))),
556- ("properties", JSON.Encode.object(Dict.fromArray([
557- ("name", JSON.Encode.string(`Test ${idx->Int.toString}`)),
558- ("gid", JSON.Encode.string(`test:${idx->Int.toString}`)),
559- ("layer", JSON.Encode.string(layerStr)),
560- ("coarse_location", JSON.Encode.string("Test City"))
561- ])))
562- ]))
563- })
564-
565- let firstFeature = switch features[0] {
566- | Some(f) => f
567- | None => JSON.Encode.null
568- }
569- let responseJson = JSON.Encode.object(Dict.fromArray([
570- ("type", JSON.Encode.string("FeatureCollection")),
571- ("geocoding", JSON.Encode.object(Dict.fromArray([
572- ("attribution", JSON.Encode.string("test"))
573- ]))),
574- ("features", JSON.Encode.array([firstFeature]))
575- ]))
576- mockFetchJson(responseJson, 200, "OK")
577-
578- let result = await Stadia.Place.getDetails(~params={
579- gid: "test:0"
580- })
581-
582- switch result {
583- | Found(place) => t->expect(place.layer)->toEqual(#poi)
584- | _ => t->expect(true)->toEqual(false)
585- }
586- })
587-})