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}