Files

193 lines
4.6 KiB
Go

// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
// gen generates internal/iatadb/db.go from the OurAirports airports.csv dataset.
//
// Usage (from repo root):
//
// go run ./internal/iatadb/gen
//
// The source CSV is read from internal/iatadb/gen/airports.csv by default.
// To use a different file, set the AIRPORTS_CSV environment variable.
// To fetch a fresh copy from OurAirports, delete airports.csv first:
//
// rm internal/iatadb/gen/airports.csv && go run ./internal/iatadb/gen
//
// The upstream dataset is published at:
//
// https://raw.githubusercontent.com/davidmegginson/ourairports-data/main/airports.csv
package main
import (
"encoding/csv"
"fmt"
"io"
"log"
"net/http"
"os"
"sort"
"strings"
"text/template"
"time"
)
const (
sourceURL = "https://raw.githubusercontent.com/davidmegginson/ourairports-data/main/airports.csv"
localCSV = "internal/iatadb/gen/airports.csv"
outputPath = "internal/iatadb/db.go"
colContinent = 7
colCountry = 8
colIATACode = 13
)
type entry struct {
IATA string
Country string
Continent string
}
func main() {
// ensure we run from the repo root
if _, err := os.Stat("go.mod"); err != nil {
fmt.Fprintf(os.Stderr, "error: run from repo root (go.mod not found)\n")
os.Exit(1)
}
var r io.Reader
csvPath := os.Getenv("AIRPORTS_CSV")
if csvPath == "" {
csvPath = localCSV
}
if _, err := os.Stat(csvPath); err == nil {
f, err := os.Open(csvPath)
if err != nil {
log.Fatalf("open %s: %v", csvPath, err)
}
defer f.Close()
r = f
log.Printf("gen: reading from %s", csvPath)
} else {
log.Printf("gen: %s not found, fetching from %s", csvPath, sourceURL)
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Get(sourceURL)
if err != nil {
log.Fatalf("fetch: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Fatalf("fetch: HTTP %d", resp.StatusCode)
}
// save locally for future runs
if err := saveLocal(resp.Body, csvPath); err != nil {
log.Printf("gen: warning: could not save local copy: %v", err)
}
f, err := os.Open(csvPath)
if err != nil {
log.Fatalf("re-open %s: %v", csvPath, err)
}
defer f.Close()
r = f
}
cr := csv.NewReader(r)
cr.LazyQuotes = true
if _, err := cr.Read(); err != nil { // skip header
log.Fatalf("read header: %v", err)
}
var entries []entry
for {
row, err := cr.Read()
if err == io.EOF {
break
}
if err != nil {
log.Printf("gen: skipping malformed row: %v", err)
continue
}
if len(row) <= colIATACode {
continue
}
iata := strings.TrimSpace(row[colIATACode])
if len(iata) != 3 {
continue
}
country := strings.TrimSpace(row[colCountry])
continent := strings.TrimSpace(row[colContinent])
if country == "" || continent == "" {
continue
}
entries = append(entries, entry{IATA: iata, Country: country, Continent: continent})
}
sort.Slice(entries, func(i, j int) bool { return entries[i].IATA < entries[j].IATA })
// deduplicate
seen := make(map[string]struct{})
unique := entries[:0]
for _, e := range entries {
if _, ok := seen[e.IATA]; !ok {
seen[e.IATA] = struct{}{}
unique = append(unique, e)
}
}
entries = unique
log.Printf("gen: %d valid IATA codes", len(entries))
out, err := os.Create(outputPath)
if err != nil {
log.Fatalf("create %s: %v", outputPath, err)
}
defer out.Close()
if err := tmpl.Execute(out, struct {
Source string
Entries []entry
}{Source: sourceURL, Entries: entries}); err != nil {
log.Fatalf("render: %v", err)
}
log.Printf("gen: wrote %s", outputPath)
}
func saveLocal(r io.Reader, path string) error {
data, err := io.ReadAll(r)
if err != nil {
return err
}
return os.WriteFile(path, data, 0644)
}
var tmpl = template.Must(template.New("").Parse(`// Code generated by internal/iatadb/gen; DO NOT EDIT.
// Source: {{ .Source }}
// To regenerate: go run ./internal/iatadb/gen
package iatadb
// Entry holds geographic metadata for a single IATA airport code.
type Entry struct {
Country string // ISO 3166-1 alpha-2 country code e.g. "CA"
Continent string // two-letter continent code: AF, AN, AS, EU, NA, OC, SA
}
// DB maps 3-letter IATA codes to geographic metadata.
// Generated from the OurAirports dataset; run go generate to refresh.
var DB = map[string]Entry{
{{ range .Entries }} "{{ .IATA }}": {Country: "{{ .Country }}", Continent: "{{ .Continent }}"},
{{ end }}}
// CountryContinent maps ISO 3166-1 alpha-2 country codes to continent codes.
// Derived from the same OurAirports dataset as DB.
var CountryContinent = func() map[string]string {
m := make(map[string]string, 250)
for _, e := range DB {
m[e.Country] = e.Continent
}
return m
}()
`))