Newer
Older
minerva / Userland / Applications / Weather / openweathermap.jakt
@minerva minerva on 13 Jul 9 KB Initial commit
import extern "LibCore/Promise.h" {
    namespace Core {
        class Promise<T> {
            [[name=try_create]]
            public extern fn Promise() throws -> Promise<T>
            public extern fn await(mut this) throws -> T
            public extern fn reject(mut this, anon error: Error) -> void
            public extern fn map<U>(mut this, anon f: fn(anon x: &mut T) -> U) -> Promise<U>
            public extern fn is_resolved(this) -> bool
        }
    }
}

use Core::Promise

import extern "AK/JsonObject.h"
import extern "AK/JsonParser.h"
import extern "AK/JsonPath.h"

import jakt::prelude::prelude

import weather { Coords, Forecast, Icon, Temperature, WeatherData }

type AK::ByteString implements(ThrowingFromStringLiteral) {}
// Hack!
trait OptionalHack<T> {
    fn value(this) -> T
}
type AK::Optional implements(OptionalHack<T>) {
    extern fn value(this) -> T
    extern fn has_value(this) -> bool
}

type AK::StringView implements(FromStringLiteral) {}

// FIXME: Our StringView does not have the same semantics as AK::StringView, this function allows passing "StringViews" to functions that expect it.
unsafe fn view(anon s: &String) -> AK::StringView {
    unsafe { cpp { "return s.view();" } }
    abort()
}

class RequestData {
    public promise: Core::Promise<prelude::String>
}

struct OpenWeatherMap {
    api_key: String
    make_request: fn(anon url: prelude::String) throws -> RequestData

    fn search(this, query: String) throws -> WeatherData {
        let make_request = &.make_request

        let url = format("https://api.openweathermap.org/data/2.5/weather?q={}&appid={}&units=metric", query, .api_key)
        mut request = make_request(url)
        let json = request.promise.await()
        mut parser = unsafe AK::JsonParser(view(&json))
        let root = parser.parse()
        return parse_data(&root)
    }

    fn search(this, coords: Coords) throws -> WeatherData {
        let make_request = &.make_request

        let url = format("https://api.openweathermap.org/data/2.5/weather?lat={}&lon={}&appid={}&units=metric", coords.latitude, coords.longitude, .api_key)
        mut request = make_request(url)
        let json = request.promise.await()
        mut parser = unsafe AK::JsonParser(view(&json))
        let root = parser.parse()
        return parse_data(&root)
    }

    fn forecast(this, data: &WeatherData) throws -> Forecast {
        let make_request = &.make_request
        let url = format(
            "https://api.openweathermap.org/data/2.5/forecast?lat={}&lon={}&appid={}&units=metric"
            data.coords.latitude
            data.coords.longitude
            .api_key
        )
        mut request = make_request(url)
        let json = request.promise.await()
        mut parser = unsafe AK::JsonParser(view(&json))
        let root = parser.parse()
        if not root.is_object() {
            throw Error::from_string_literal("Expected JSON object")
        }

        mut forecast = Forecast(data: [])

        let maybe_list = root.as_object().get_array("list")
        if not maybe_list.has_value() {
            throw Error::from_string_literal("Expected 'forecast::list' to be an array")
        }

        let list = maybe_list.value()
        for i in 0..list.size() {
            let data = parse_data(&list.at(i), parse_forecast: true)
            forecast.data.push(data)
        }

        return forecast
    }

    fn find(this, query: String) throws -> Core::Promise<[String:Coords]> {
        let make_request = &.make_request

        let url = format("https://api.openweathermap.org/geo/1.0/direct?q={}&appid={}&limit=10", query, .api_key)
        mut request = make_request(url)
        return request.promise.map(fn[request](anon json: &mut prelude::String) -> [String:Coords] {
            // FIXME: Can't `try{} catch{}` the whole block for some reason?
            mut parser = unsafe AK::JsonParser(view(&json))
            let root = try parser.parse() catch { return [:] }
            if not root.is_array() { return [:] }

            mut result: [String:Coords] = [:]
            let root_array = root.as_array()

            for i in 0..root_array.size() {
                let object = root_array.at(i)
                if not object.is_object() {
                    continue
                }

                let name = try unsafe(*at_path(&object, [AK::JsonPathElement(view(&"name"))]).as_string()) as! String catch { return [:] }
                let country = try unsafe(*at_path(&object, [AK::JsonPathElement(view(&"country"))]).as_string()) as! String catch { return [:] }
                let latitude = try unsafe at_path(
                    &object
                    [AK::JsonPathElement(view(&"lat"))]
                ).get_number_with_precision_loss<f64>().value() catch { return [:] }
                let longitude = try unsafe at_path(
                    &object
                    [AK::JsonPathElement(view(&"lon"))]
                ).get_number_with_precision_loss<f64>().value() catch { return [:] }
                result.set(format("{}, {}", name, country), Coords(latitude, longitude))
            }

            return result
        })
    }

    private fn parse_data<T>(anon root: &T, parse_forecast: bool = false) throws -> WeatherData {
        mut city_name = ""
        mut country_code = ""
        mut latitude = 0.0
        mut longitude = 0.0

        if not parse_forecast {
            city_name = unsafe(*at_path(
                &root
                [AK::JsonPathElement(view(&"name"))]
            ).as_string()) as! String

            country_code = unsafe(*at_path(
                &root
                [AK::JsonPathElement(view(&"sys")), AK::JsonPathElement(view(&"country"))]
            ).as_string()) as! String

            latitude = unsafe at_path(
                &root
                [AK::JsonPathElement(view(&"coord")), AK::JsonPathElement(view(&"lat"))]
            ).get_number_with_precision_loss<f64>().value()

            longitude = unsafe at_path(
                &root
                [AK::JsonPathElement(view(&"coord")), AK::JsonPathElement(view(&"lon"))]
            ).get_number_with_precision_loss<f64>().value()
        }

        let state = unsafe (*at_path(
            &root
            [AK::JsonPathElement(view(&"weather")), AK::JsonPathElement(0), AK::JsonPathElement(view(&"main"))]
        ).as_string()) as! String

        let temperature = Temperature(
            current: unsafe at_path(
                &root
                [AK::JsonPathElement(view(&"main")), AK::JsonPathElement(view(&"temp"))]
            ).get_number_with_precision_loss<f64>().value(),
            min: unsafe at_path(
                &root
                [AK::JsonPathElement(view(&"main")), AK::JsonPathElement(view(&"temp_min"))]
            ).get_number_with_precision_loss<f64>().value(),
            max: unsafe at_path(
                &root
                [AK::JsonPathElement(view(&"main")), AK::JsonPathElement(view(&"temp_max"))]
            ).get_number_with_precision_loss<f64>().value(),
            feels_like: unsafe at_path(
                &root
                [AK::JsonPathElement(view(&"main")), AK::JsonPathElement(view(&"feels_like"))]
            ).get_number_with_precision_loss<f64>().value()
        )

        let icon_name = unsafe (*at_path(
            &root
            [AK::JsonPathElement(view(&"weather")), AK::JsonPathElement(0), AK::JsonPathElement(view(&"icon"))]
        ).as_string()) as! String

        let timestamp = unsafe at_path(
            &root
            [AK::JsonPathElement(view(&"dt"))]
        ).get_number_with_precision_loss<i64>().value()

        let cloudiness = unsafe at_path(
            &root
            [AK::JsonPathElement(view(&"clouds")), AK::JsonPathElement(view(&"all"))]
        ).get_number_with_precision_loss<f64>().value()

        let humidity = unsafe at_path(
            &root
            [AK::JsonPathElement(view(&"main")), AK::JsonPathElement(view(&"humidity"))]
        ).get_number_with_precision_loss<f64>().value()

        return WeatherData(
            city_name
            country_code
            coords: Coords(latitude, longitude)
            state
            cloudiness
            precipitation: 0f64
            humidity
            temperature
            icon: match icon_name {
                "01d" => Icon::Clear(day: true)
                "01n" => Icon::Clear(day: false)
                "02d" => Icon::PartiallyCloudy(day: true)
                "02n" => Icon::PartiallyCloudy(day: false)
                "03d" => Icon::Cloudy(day: true)
                "03n" => Icon::Cloudy(day: false)
                "04d" => Icon::Cloudy(day: true)
                "04n" => Icon::Cloudy(day: false)
                "09d" => Icon::Rainy(day: true)
                "09n" => Icon::Rainy(day: false)
                "10d" => Icon::Rainy(day: true)
                "10n" => Icon::Rainy(day: false)
                "11d" => Icon::Thunderstorm(day: true)
                "11n" => Icon::Thunderstorm(day: false)
                "13d" => Icon::Snowy(day: true)
                "13n" => Icon::Snowy(day: false)
                "50d" => Icon::Foggy(day: true)
                "50n" => Icon::Foggy(day: false)
                else => Icon::Unknown(day: true)
            }
            timestamp
        )
    }

    private fn at_path<T>(anon object: &T, anon elements: [AK::JsonPathElement]) throws -> AK::JsonValue {
        mut path = AK::JsonPath()
        for element in elements {
            path.append(element)
        }
        return path.try_resolve(object)
    }
}