Waldo.res
1// Internal helpers (not exposed in .resi)
2
3let encodeURIComponent = (str: string): string => {
4 %raw(`encodeURIComponent`)(str)
5}
6
7/** Single geocoding result from Waldo API
8 * Matches the Go waldo.Result struct and OpenAPI spec
9 */
10type result = {
11 gid: int,
12 name: string,
13 placetype: string,
14 lon: float,
15 lat: float,
16 distance: float,
17 bearing: float,
18 region: array<string>,
19 country: string,
20}
21
22/** GeocodeResponse from Waldo API
23 * Contains results array and count
24 */
25type geocodeResponse = {
26 results: array<result>,
27 numResults: int,
28}
29
30/** Error response from Waldo API */
31type errorResponse = {
32 error: string,
33}
34
35/** Response variant for type-safe handling */
36type response =
37 | Success(array<result>)
38 | Error(string)
39 | NetworkError(string)
40
41/** Decode a single result from JSON
42 * Expected shape: {"gid": int, "name": string, "placetype": string, "lon": float, "lat": float, "distance": float, "bearing": float, "region": array<string>, "country": string}
43 */
44let decodeResult: JSON.t => option<result> = json => {
45 switch json->JSON.Decode.object {
46 | Some(dict) => {
47 let gid =
48 dict->Dict.get("gid")->Option.flatMap(JSON.Decode.float)->Option.map(f => f->Float.toInt)
49 let name = dict->Dict.get("name")->Option.flatMap(JSON.Decode.string)
50 let placetype = dict->Dict.get("placetype")->Option.flatMap(JSON.Decode.string)
51 let lon = dict->Dict.get("lon")->Option.flatMap(JSON.Decode.float)
52 let lat = dict->Dict.get("lat")->Option.flatMap(JSON.Decode.float)
53 let distance = dict->Dict.get("distance")->Option.flatMap(JSON.Decode.float)
54 let bearing = dict->Dict.get("bearing")->Option.flatMap(JSON.Decode.float)
55 let region =
56 dict
57 ->Dict.get("region")
58 ->Option.flatMap(JSON.Decode.array)
59 ->Option.map(arr => arr->Array.filterMap(JSON.Decode.string))
60 let country = dict->Dict.get("country")->Option.flatMap(JSON.Decode.string)
61
62 switch (gid, name, placetype, lon, lat, distance, bearing, region, country) {
63 | (
64 Some(gid),
65 Some(name),
66 Some(placetype),
67 Some(lon),
68 Some(lat),
69 Some(distance),
70 Some(bearing),
71 Some(region),
72 Some(country),
73 ) =>
74 Some({gid, name, placetype, lon, lat, distance, bearing, region, country})
75 | _ => {
76 // DEBUG: Log which fields are missing
77 let missing = []
78 if gid->Option.isNone {
79 missing->Array.push("gid")
80 }
81 if name->Option.isNone {
82 missing->Array.push("name")
83 }
84 if placetype->Option.isNone {
85 missing->Array.push("placetype")
86 }
87 if lon->Option.isNone {
88 missing->Array.push("lon")
89 }
90 if lat->Option.isNone {
91 missing->Array.push("lat")
92 }
93 if distance->Option.isNone {
94 missing->Array.push("distance")
95 }
96 if bearing->Option.isNone {
97 missing->Array.push("bearing")
98 }
99 if region->Option.isNone {
100 missing->Array.push("region")
101 }
102 if country->Option.isNone {
103 missing->Array.push("country")
104 }
105 if missing->Array.length > 0 {
106 Console.error(`[Waldo] decodeResult: Missing fields: ${missing->Array.join(", ")}`)
107 }
108 None
109 }
110 }
111 }
112 | None => {
113 Console.error("[Waldo] decodeResult: Input is not a JSON object")
114 None
115 }
116 }
117}
118
119/** Decode geocode response from JSON
120 * Expected shape: {"results": array<result>, "num_results": int}
121 */
122let decodeGeocodeResponse: JSON.t => option<geocodeResponse> = json => {
123 switch json->JSON.Decode.object {
124 | Some(dict) => {
125 let results =
126 dict
127 ->Dict.get("results")
128 ->Option.flatMap(JSON.Decode.array)
129 ->Option.map(arr => arr->Array.filterMap(decodeResult))
130 let numResults =
131 dict
132 ->Dict.get("num_results")
133 ->Option.flatMap(JSON.Decode.float)
134 ->Option.map(f => f->Float.toInt)
135
136 switch (results, numResults) {
137 | (Some(results), Some(numResults)) => Some({results, numResults})
138 | _ => {
139 let missing = []
140 if results->Option.isNone {
141 missing->Array.push("results")
142 }
143 if numResults->Option.isNone {
144 missing->Array.push("num_results")
145 }
146 Console.error(
147 `[Waldo] decodeGeocodeResponse: Missing fields: ${missing->Array.join(", ")}`,
148 )
149 None
150 }
151 }
152 }
153 | None => {
154 Console.error("[Waldo] decodeGeocodeResponse: Input is not a JSON object")
155 None
156 }
157 }
158}
159
160/** Decode error response from JSON
161 * Expected shape: {"error": string}
162 */
163let decodeErrorResponse: JSON.t => option<errorResponse> = json => {
164 switch json->JSON.Decode.object {
165 | Some(dict) => switch dict->Dict.get("error")->Option.flatMap(JSON.Decode.string) {
166 | Some(error) => Some({error: error})
167 | None => None
168 }
169 | None => None
170 }
171}
172
173module Forward = {
174 type params = {
175 query: string,
176 biasPoint?: (float, float), // (lon, lat)
177 }
178
179 let buildUrl = (params: params): string => {
180 let encodedQuery = encodeURIComponent(params.query)
181 let queryParts = [`q=${encodedQuery}`]
182
183 switch params.biasPoint {
184 | Some((lon, lat)) => {
185 queryParts->Array.push(`lon=${Float.toString(lon)}`)
186 queryParts->Array.push(`lat=${Float.toString(lat)}`)
187 }
188 | None => ()
189 }
190
191 `/forward?${queryParts->Array.join("&")}`
192 }
193
194 let geocode = async (~params: params): response => {
195 let url = buildUrl(params)
196
197 try {
198 let response = await Fetch.get(url)
199 ->Fetch.onResponse(
200 ~onSuccess=json => json,
201 ~onError=(status, message) => {
202 JSON.Encode.object(
203 Dict.fromArray([
204 ("__error", JSON.Encode.bool(true)),
205 ("__status", JSON.Encode.float(Int.toFloat(status))),
206 ("__message", JSON.Encode.string(message)),
207 ]),
208 )
209 },
210 )
211 ->Fetch.send
212
213 // DEBUG: Log the actual response content
214 let responseStr = try {
215 JSON.stringify(response)
216 } catch {
217 | _ => "[Failed to stringify response]"
218 }
219 Log.debug(`[Waldo] Forward: Response content: ${responseStr}`)
220
221 // Check if we got an error response from Fetch
222 switch response->JSON.Decode.object {
223 | Some(obj) =>
224 switch obj->Dict.get("__error") {
225 | Some(_) => {
226 let status = switch obj->Dict.get("__status") {
227 | Some(s) =>
228 switch s->JSON.Decode.float {
229 | Some(n) => n->Float.toInt->Int.toString
230 | None => "0"
231 }
232 | None => "0"
233 }
234 let message = switch obj->Dict.get("__message") {
235 | Some(m) =>
236 switch m->JSON.Decode.string {
237 | Some(s) => s
238 | None => "Unknown error"
239 }
240 | None => "Unknown error"
241 }
242 Console.error(`[Waldo] Forward: Fetch error: ${status} - ${message}`)
243 Error(`${status}: ${message}`)
244 }
245 | None => // Try to decode as success response
246 switch decodeGeocodeResponse(response) {
247 | Some(resp) => Success(resp.results)
248 | None => {
249 Console.error("[Waldo] Forward: Failed to decode geocode response")
250 // Try to decode as error response
251 switch decodeErrorResponse(response) {
252 | Some(err) => {
253 Console.error(`[Waldo] Forward: API error: ${err.error}`)
254 Error(err.error)
255 }
256 | None => {
257 Console.error("[Waldo] Forward: Unknown response format")
258 Error("Invalid response format")
259 }
260 }
261 }
262 }
263 }
264 | None => {
265 Console.error("[Waldo] Forward: Response is not a valid JSON object")
266 Error("Invalid JSON response")
267 }
268 }
269 } catch {
270 | exn => {
271 Console.error2("[Waldo] Forward: Network error or exception:", exn)
272 NetworkError("Network error")
273 }
274 }
275 }
276}
277
278module Reverse = {
279 type params = {
280 lon: float,
281 lat: float,
282 }
283
284 let buildUrl = (params: params): string => {
285 let lonStr = Float.toString(params.lon)
286 let latStr = Float.toString(params.lat)
287 `/reverse?lon=${lonStr}&lat=${latStr}`
288 }
289
290 let geocode = async (~params: params): response => {
291 let url = buildUrl(params)
292
293 try {
294 let response = await Fetch.get(url)
295 ->Fetch.onResponse(
296 ~onSuccess=json => json,
297 ~onError=(status, message) => {
298 JSON.Encode.object(
299 Dict.fromArray([
300 ("__error", JSON.Encode.bool(true)),
301 ("__status", JSON.Encode.float(Int.toFloat(status))),
302 ("__message", JSON.Encode.string(message)),
303 ]),
304 )
305 },
306 )
307 ->Fetch.send
308
309 // Check if we got an error response from Fetch
310 switch response->JSON.Decode.object {
311 | Some(obj) =>
312 switch obj->Dict.get("__error") {
313 | Some(_) => {
314 let status = switch obj->Dict.get("__status") {
315 | Some(s) =>
316 switch s->JSON.Decode.float {
317 | Some(n) => n->Float.toInt->Int.toString
318 | None => "0"
319 }
320 | None => "0"
321 }
322 let message = switch obj->Dict.get("__message") {
323 | Some(m) =>
324 switch m->JSON.Decode.string {
325 | Some(s) => s
326 | None => "Unknown error"
327 }
328 | None => "Unknown error"
329 }
330 Error(`${status}: ${message}`)
331 }
332 | None => // Try to decode as success response
333 switch decodeGeocodeResponse(response) {
334 | Some(resp) => Success(resp.results)
335 | None => // Try to decode as error response
336 switch decodeErrorResponse(response) {
337 | Some(err) => Error(err.error)
338 | None => Error("Invalid response format")
339 }
340 }
341 }
342 | None => Error("Invalid JSON response")
343 }
344 } catch {
345 | _ => NetworkError("Network error")
346 }
347 }
348}