metar-http 0.0.6 → 0.0.7
raw patch · 3 files changed
+214/−33 lines, 3 filesdep ~metarPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: metar
API changes (from Hackage documentation)
Files
- changelog.md +13/−0
- metar-http.cabal +2/−2
- src/Data/Aviation/Metar/Http.hs +199/−31
changelog.md view
@@ -1,3 +1,16 @@+0.0.7++* Add `/taf/<icao>/...` endpoints mirroring the `/metar` shape+ * Same one-line/max-lines/truncate/append modifiers as `/metar`+ * BOM for `Y*` ICAOs, NOAA fallback for the rest (via `getTAF` in+ `metar-0.0.7`)+* Replace the plain-text `/` index with a self-contained HTML page+ * Documents every endpoint with worked `curl` examples+ * Explicit per-level table of valid GPWT areas+ * Light/dark aware CSS, system-font stack, mobile viewport+ * The 404 response body reuses the same HTML page+* Require `metar >= 0.0.7`+ 0.0.6 * Add BOM Graphical Area Forecast (GAF) endpoints
metar-http.cabal view
@@ -1,5 +1,5 @@ name: metar-http-version: 0.0.6+version: 0.0.7 license: BSD3 license-file: LICENCE author: Tony Morris <ʇǝu˙sıɹɹoɯʇ@sıɹɹoɯʇ>@@ -27,7 +27,7 @@ , bytestring >= 0.10 && < 0.13 , http-types >= 0.9 && < 1 , lens >= 4 && < 6- , metar >= 0.0.6 && < 1+ , metar >= 0.0.7 && < 1 , network-uri >= 2.6 && < 3 , text >= 1.2 && < 3 , semigroups >= 0.9 && < 1
src/Data/Aviation/Metar/Http.hs view
@@ -24,9 +24,11 @@ import Control.Lens (folded, (^.), (^?), _Wrapped) import Data.Aviation.GAF (GAFError (GAFHttpError, GAFUnknownArea), GAFImage (GAFImage), GAFPeriod (GAFCurrent, GAFNext), getGAF, renderGAFError) import Data.Aviation.GPWT (GPWTError (GPWTHttpError, GPWTNoSuchProduct, GPWTUnknownLevel), GPWTImage (GPWTImage), getGPWT, parseLevel, renderGPWTError)-import Data.Aviation.Metar (getMETAR)+import Data.Aviation.Metar (getMETAR, getTAF) import Data.Aviation.Metar.METARResult (_METARResultValue)+import Data.Aviation.Metar.METARResultT (METARResultT) import qualified Data.ByteString.Char8 as BS+import qualified Data.ByteString.Lazy as BL import Data.ByteString.Lazy.UTF8 (fromString) import Data.List (intercalate) import Data.Text (Text, toLower, unpack)@@ -208,35 +210,15 @@ Just l -> MaxLines l r' --- | WAI 'Application' serving METAR observations and BOM GAF images.+-- | WAI 'Application' serving METAR observations and BOM GAF/GPWT images. metarHTTPapp :: Application metarHTTPapp req withResp =- let msg =- let a </> b =- a <> "\n" <> b- a <//> b =- a </> "\n" <> b- in "/metar/<icao>"- </> "raw metar for station <icao>"- <//> "/metar/<icao>/*"- </> "metar for station <icao> all on one line"- <//> "/metar/<icao>/*/<maxchars>"- </> "metar for station <icao> all on one line truncated at <maxchars>"- <//> "/metar/<icao>/*/<maxchars>/<appendstr>"- </> "metar for station <icao> all on one line truncated at <maxchars> and if truncation occurs, append <appendstr>"- <//> "/gaf/<area>/current"- </> "current BOM Graphical Area Forecast (PNG) for <area> (WA-N, WA-S, NT, QLD-N, QLD-S, SA, NSW-W, NSW-E, VIC, TAS)"- <//> "/gaf/<area>/next"- </> "next BOM Graphical Area Forecast (PNG) for <area>"- <//> "/gpwt/<level>/<area>/<time>"- </> "BOM Grid Point Wind & Temperature forecast (PNG); <level> is low, mid or high; <area> e.g. AUS, NSW, QLD-N, VIC-TAS, TIMS; <time> is a 3-hourly UTC slot like 00Z, 03Z ... 21Z"- <//> ""- _404 =+ let _404 = responseLBS status404- []- msg+ [(hContentType, htmlContentType)]+ indexHTML in case pathInfo req of ["gaf", area, periodTxt] | Just period <- gafPeriod (toLower periodTxt) ->@@ -257,32 +239,53 @@ r <- getGPWT lv (unpack area) (unpack timeTxt) withResp (gpwtResponse r) (rpt : xxxx : r)- | toLower rpt == "metar" ->+ | Just (rptName, fetch) <- reportKind rpt -> let xxxx' = unpack xxxx modifyOutput = format (uriPathFormat (unpack <$> r)) in do- t <- getMETAR xxxx' ^. _Wrapped+ t <- fetch xxxx' ^. _Wrapped withResp $ case t ^? _METARResultValue of Nothing -> responseLBS status404 []- ("no METAR found for " <> fromString xxxx')+ ("no " <> fromString rptName <> " found for " <> fromString xxxx') Just x -> responseLBS status200 [(hContentType, "text/plain")]- (fromString (modifyOutput [x]))+ (fromString (modifyOutput (lines x))) [] -> withResp $ responseLBS status200- [(hContentType, "text/plain")]- msg+ [(hContentType, htmlContentType)]+ indexHTML _ -> withResp _404 +-- | Dispatch table for the text-report routes (@/metar/@ and @/taf/@).+-- Returns the display label used in \"no X found\" errors and the fetcher+-- to call, or 'Nothing' if the path prefix is not a known report kind.+--+-- >>> import Data.Aviation.Metar.METARResultT (METARResultT)+-- >>> import Data.Text (pack)+-- >>> fmap fst (reportKind (pack "metar"))+-- Just "METAR"+-- >>> fmap fst (reportKind (pack "TAF"))+-- Just "TAF"+-- >>> fmap fst (reportKind (pack "other"))+-- Nothing+reportKind ::+ Text ->+ Maybe (String, String -> METARResultT IO String)+reportKind rpt =+ case toLower rpt of+ "metar" -> Just ("METAR", getMETAR)+ "taf" -> Just ("TAF", getTAF)+ _ -> Nothing+ -- | Parse the trailing path segment of a @/gaf/<area>/...@ request into a -- 'GAFPeriod'. --@@ -380,3 +383,168 @@ (q : _) -> maybe id setPort (readMaybe q) runSettings (setTimeout 6 (p defaultSettings)) metarHTTPapp++-- | @text/html; charset=utf-8@ as a strict 'BS.ByteString'.+htmlContentType ::+ BS.ByteString+htmlContentType =+ "text/html; charset=utf-8"++-- | HTML documentation page served at @/@ (and as the body of any @404@).+-- Describes every endpoint the server exposes, with worked @curl@ examples.+indexHTML ::+ BL.ByteString+indexHTML =+ fromString $+ unlines+ [ "<!doctype html>"+ , "<html lang=\"en\">"+ , "<head>"+ , "<meta charset=\"utf-8\">"+ , "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"+ , "<title>metar-http</title>"+ , "<style>"+ , ":root { color-scheme: light dark;"+ , " --bg:#fafbfc; --fg:#24292f; --muted:#57606a; --accent:#0969da;"+ , " --code-bg:#eff1f3; --border:#d0d7de; }"+ , "@media (prefers-color-scheme: dark) { :root {"+ , " --bg:#0d1117; --fg:#e6edf3; --muted:#8b949e; --accent:#4493f8;"+ , " --code-bg:#161b22; --border:#30363d; } }"+ , "* { box-sizing: border-box; }"+ , "body { margin:0; padding:2.5rem 1.25rem 3rem;"+ , " font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,sans-serif;"+ , " color:var(--fg); background:var(--bg); line-height:1.55; }"+ , "main { max-width:780px; margin:0 auto; }"+ , "h1 { font-size:1.85rem; letter-spacing:-0.01em; margin:0 0 0.25rem; }"+ , ".tagline { color:var(--muted); margin:0 0 2rem; font-size:1.02rem; }"+ , "section { margin:2rem 0 0; padding-top:1.75rem; border-top:1px solid var(--border); }"+ , "h2 { font-size:1.2rem; margin:0 0 0.6rem; }"+ , "h3 { font-size:1rem; margin:1.4rem 0 0.5rem; color:var(--muted);"+ , " text-transform:uppercase; letter-spacing:0.06em; font-weight:600; }"+ , "p { margin:0.55rem 0; }"+ , "code, pre, .endpoints dt {"+ , " font-family:ui-monospace,'SF Mono',Menlo,Consolas,'Liberation Mono',monospace;"+ , " font-size:0.925em; }"+ , "code { background:var(--code-bg); padding:0.12em 0.38em; border-radius:3px; }"+ , "pre { background:var(--code-bg); border:1px solid var(--border); border-radius:6px;"+ , " padding:0.85rem 1rem; margin:0.6rem 0; overflow-x:auto; line-height:1.5; }"+ , "pre code { background:transparent; padding:0; }"+ , ".endpoints { margin:0.75rem 0; }"+ , ".endpoints dt { font-weight:600; margin-top:0.8rem;"+ , " color:var(--accent); }"+ , ".endpoints dt:first-child { margin-top:0; }"+ , ".endpoints dd { margin:0.2rem 0 0 1.5rem; color:var(--muted); }"+ , ".codes { display:flex; flex-wrap:wrap; gap:0.35rem; margin:0.5rem 0 0.75rem; }"+ , ".codes code { font-size:0.85em; }"+ , "table.areas { border-collapse:collapse; margin:0.5rem 0 0.75rem; }"+ , "table.areas th, table.areas td { padding:0.35rem 0.9rem 0.35rem 0;"+ , " border-bottom:1px solid var(--border); vertical-align:top; text-align:left; }"+ , "table.areas th { font-weight:600; white-space:nowrap; padding-right:1.25rem; }"+ , "table.areas td code, table.areas th code { font-size:0.85em; }"+ , "table.areas tr:last-child th, table.areas tr:last-child td { border-bottom:0; }"+ , "footer { color:var(--muted); font-size:0.85rem; margin-top:3rem;"+ , " padding-top:1.25rem; border-top:1px solid var(--border); }"+ , "a { color:var(--accent); }"+ , "a:hover { text-decoration:underline; }"+ , "</style>"+ , "</head>"+ , "<body>"+ , "<main>"+ , "<h1>metar-http</h1>"+ , "<p class=\"tagline\">Aviation weather products \8212 METAR observations, TAF forecasts, Graphical Area Forecasts and Grid Point Wind & Temperature charts \8212 from the Australian Bureau of Meteorology, with NOAA fallback for non-Australian aerodromes.</p>"+ , ""+ , "<section>"+ , "<h2>METAR observations</h2>"+ , "<p>Fetch the current METAR/SPECI observation for an aerodrome by ICAO code. Australian aerodromes (codes beginning with <code>Y</code>) come from BOM; others fall back to NOAA.</p>"+ , "<dl class=\"endpoints\">"+ , "<dt>GET /metar/<icao></dt>"+ , "<dd>Raw METAR (may span multiple lines).</dd>"+ , "<dt>GET /metar/<icao>/*</dt>"+ , "<dd>METAR collapsed onto one line.</dd>"+ , "<dt>GET /metar/<icao>/*/<maxchars></dt>"+ , "<dd>One line, truncated to <code><maxchars></code> characters.</dd>"+ , "<dt>GET /metar/<icao>/*/<maxchars>/<append></dt>"+ , "<dd>One line, truncated to <code><maxchars></code>; if truncation occurred, <code><append></code> is appended.</dd>"+ , "<dt>GET /metar/<icao>/<maxlines></dt>"+ , "<dd>First <code><maxlines></code> lines.</dd>"+ , "<dt>GET /metar/<icao>/<maxlines>/<maxchars></dt>"+ , "<dd>First <code><maxlines></code> lines, each truncated to <code><maxchars></code>.</dd>"+ , "<dt>GET /metar/<icao>/<maxlines>/<maxchars>/<append></dt>"+ , "<dd>First <code><maxlines></code> lines, truncated with <code><append></code> suffix on truncation.</dd>"+ , "</dl>"+ , "<h3>Examples</h3>"+ , "<pre><code>curl http://localhost:8080/metar/YSSY"+ , "curl http://localhost:8080/metar/YBAF/*"+ , "curl http://localhost:8080/metar/KJFK/1/80/...</code></pre>"+ , "</section>"+ , ""+ , "<section>"+ , "<h2>TAF forecasts</h2>"+ , "<p>Fetch the current TAF (Terminal Aerodrome Forecast) for an aerodrome by ICAO code. Australian aerodromes (codes beginning with <code>Y</code>) come from BOM; others fall back to NOAA. TAFs typically span several lines \8212 the same truncation/one-line options as <code>/metar</code> apply.</p>"+ , "<dl class=\"endpoints\">"+ , "<dt>GET /taf/<icao></dt>"+ , "<dd>Raw TAF (multi-line).</dd>"+ , "<dt>GET /taf/<icao>/*</dt>"+ , "<dd>TAF collapsed onto one line.</dd>"+ , "<dt>GET /taf/<icao>/*/<maxchars></dt>"+ , "<dd>One line, truncated to <code><maxchars></code> characters.</dd>"+ , "<dt>GET /taf/<icao>/*/<maxchars>/<append></dt>"+ , "<dd>One line, truncated to <code><maxchars></code>; if truncation occurred, <code><append></code> is appended.</dd>"+ , "<dt>GET /taf/<icao>/<maxlines></dt>"+ , "<dd>First <code><maxlines></code> lines.</dd>"+ , "<dt>GET /taf/<icao>/<maxlines>/<maxchars></dt>"+ , "<dd>First <code><maxlines></code> lines, each truncated to <code><maxchars></code>.</dd>"+ , "<dt>GET /taf/<icao>/<maxlines>/<maxchars>/<append></dt>"+ , "<dd>First <code><maxlines></code> lines, truncated with <code><append></code> suffix on truncation.</dd>"+ , "</dl>"+ , "<h3>Examples</h3>"+ , "<pre><code>curl http://localhost:8080/taf/YSSY"+ , "curl http://localhost:8080/taf/YBBN/*"+ , "curl http://localhost:8080/taf/KJFK/3</code></pre>"+ , "</section>"+ , ""+ , "<section>"+ , "<h2>Graphical Area Forecasts (GAF)</h2>"+ , "<p>The current or next PNG chart for one of BOM's ten forecast areas. Which of the four rotating products counts as \"current\" or \"next\" is derived from the current UTC hour (rotations at 05, 11, 17 and 23Z).</p>"+ , "<dl class=\"endpoints\">"+ , "<dt>GET /gaf/<area>/current</dt>"+ , "<dd>Current GAF PNG for <code><area></code>.</dd>"+ , "<dt>GET /gaf/<area>/next</dt>"+ , "<dd>Next GAF PNG for <code><area></code>.</dd>"+ , "</dl>"+ , "<p>Valid areas:</p>"+ , "<p class=\"codes\"><code>WA-N</code> <code>WA-S</code> <code>NT</code> <code>QLD-N</code> <code>QLD-S</code> <code>SA</code> <code>NSW-W</code> <code>NSW-E</code> <code>VIC</code> <code>TAS</code></p>"+ , "<h3>Examples</h3>"+ , "<pre><code>curl -o wan-current.png http://localhost:8080/gaf/WA-N/current"+ , "curl -o qlds-next.png http://localhost:8080/gaf/QLD-S/next</code></pre>"+ , "</section>"+ , ""+ , "<section>"+ , "<h2>Grid Point Wind & Temperature (GPWT)</h2>"+ , "<p>PNG chart for a given flight-level band, forecast area and 3-hourly UTC time slot. The valid <em>(level, area, time)</em> triples are discovered by scraping BOM's grid-point-forecasts page; requesting a triple that doesn't exist returns <code>404</code> with the valid list.</p>"+ , "<dl class=\"endpoints\">"+ , "<dt>GET /gpwt/<level>/<area>/<time></dt>"+ , "<dd>GPWT PNG for the requested triple.</dd>"+ , "</dl>"+ , "<p><strong>Level and area (each row shows all areas available at that level):</strong></p>"+ , "<table class=\"areas\">"+ , "<tr><th><code>low</code></th><td><code>AUS</code> <code>NSW</code> <code>NT</code> <code>QLD-N</code> <code>QLD-S</code> <code>SA</code> <code>TIMS</code> <code>VIC/TAS</code> <code>WA-N</code> <code>WA-S</code></td></tr>"+ , "<tr><th><code>mid</code></th><td><code>AUS</code> <code>NE</code> <code>SE</code> <code>TASMAN-SEA</code> <code>WEST</code></td></tr>"+ , "<tr><th><code>high</code></th><td><code>AUS</code> <code>TASMAN-SEA</code></td></tr>"+ , "</table>"+ , "<p>Codes are matched case-insensitively with punctuation ignored \8212 <code>VIC/TAS</code>, <code>vic-tas</code> and <code>VICTAS</code> all resolve to the same product.</p>"+ , "<p><strong>Time:</strong> <code>00Z</code>, <code>03Z</code>, <code>06Z</code>, <code>09Z</code>, <code>12Z</code>, <code>15Z</code>, <code>18Z</code>, <code>21Z</code>.</p>"+ , "<h3>Examples</h3>"+ , "<pre><code>curl -o qldn-09z.png http://localhost:8080/gpwt/low/QLD-N/09Z"+ , "curl -o aus-mid-12z.png http://localhost:8080/gpwt/mid/AUS/12Z"+ , "curl -o tims-18z.png http://localhost:8080/gpwt/low/TIMS/18Z"+ , "curl -o tasman-00z.png http://localhost:8080/gpwt/high/TASMAN-SEA/00Z</code></pre>"+ , "</section>"+ , ""+ , "<footer>"+ , "Data sourced from the Australian Bureau of Meteorology (<a href=\"https://www.bom.gov.au/aviation/\">bom.gov.au/aviation</a>) and NOAA (<a href=\"https://tgftp.nws.noaa.gov/data/observations/metar/stations/\">tgftp.nws.noaa.gov</a>). Not for operational use \8212 refer to <a href=\"https://www.airservicesaustralia.com/\">Airservices Australia</a> for flight planning."+ , "</footer>"+ , "</main>"+ , "</body>"+ , "</html>"+ ]