Skip to main content

fraiseql_cli/schema/
lookup_data.rs

1//! Lookup data for geographic, currency, and locale-based operators.
2//!
3//! This module provides static lookup tables used by rich filter operators
4//! that require knowledge of external data (countries, currencies, timezones, etc.).
5//!
6//! # Data Structure
7//!
8//! Lookup tables are embedded as JSON in the compiled schema metadata, enabling
9//! the runtime to perform lookups without external dependencies:
10//!
11//! ```json
12//! {
13//!   "countries": {
14//!     "US": { "continent": "North America", "regions": ["Americas"], "in_eu": false, "in_schengen": false },
15//!     "FR": { "continent": "Europe", "regions": ["Europe"], "in_eu": true, "in_schengen": true },
16//!     ...
17//!   },
18//!   "currencies": {
19//!     "USD": { "name": "US Dollar", "symbol": "$" },
20//!     "EUR": { "name": "Euro", "symbol": "€" },
21//!     ...
22//!   },
23//!   "timezones": {
24//!     "UTC": { "offset": 0, "dst": false },
25//!     "EST": { "offset": -300, "dst": true },
26//!     ...
27//!   }
28//! }
29//! ```
30//!
31//! # Lookup Operators
32//!
33//! These operators use the lookup data at runtime:
34//! - Country: continent, region, EU/Schengen membership
35//! - Currency: code, symbol, decimal places
36//! - Timezone: UTC offset, daylight saving time
37//! - Language: language family
38//! - Locale: language, country, script
39
40use std::collections::HashMap;
41
42use serde_json::{Value, json};
43
44/// Build lookup data for rich filter operators.
45///
46/// Returns a JSON structure containing all lookup tables needed by operators.
47pub fn build_lookup_data() -> Value {
48    json!({
49        "countries": build_countries_lookup(),
50        "currencies": build_currencies_lookup(),
51        "timezones": build_timezones_lookup(),
52        "languages": build_languages_lookup(),
53    })
54}
55
56/// Build country code lookup table with continent, region, EU, Schengen data.
57fn build_countries_lookup() -> HashMap<String, Value> {
58    let mut countries = HashMap::new();
59
60    // Europe
61    countries.insert(
62        "FR".to_string(),
63        json!({
64            "name": "France",
65            "continent": "Europe",
66            "region": "EU",
67            "in_eu": true,
68            "in_schengen": true,
69        }),
70    );
71    countries.insert(
72        "DE".to_string(),
73        json!({
74            "name": "Germany",
75            "continent": "Europe",
76            "region": "EU",
77            "in_eu": true,
78            "in_schengen": true,
79        }),
80    );
81    countries.insert(
82        "GB".to_string(),
83        json!({
84            "name": "United Kingdom",
85            "continent": "Europe",
86            "region": "EU",
87            "in_eu": false,
88            "in_schengen": false,
89        }),
90    );
91    countries.insert(
92        "IT".to_string(),
93        json!({
94            "name": "Italy",
95            "continent": "Europe",
96            "region": "EU",
97            "in_eu": true,
98            "in_schengen": true,
99        }),
100    );
101    countries.insert(
102        "ES".to_string(),
103        json!({
104            "name": "Spain",
105            "continent": "Europe",
106            "region": "EU",
107            "in_eu": true,
108            "in_schengen": true,
109        }),
110    );
111    countries.insert(
112        "PL".to_string(),
113        json!({
114            "name": "Poland",
115            "continent": "Europe",
116            "region": "EU",
117            "in_eu": true,
118            "in_schengen": true,
119        }),
120    );
121    countries.insert(
122        "NL".to_string(),
123        json!({
124            "name": "Netherlands",
125            "continent": "Europe",
126            "region": "EU",
127            "in_eu": true,
128            "in_schengen": true,
129        }),
130    );
131    countries.insert(
132        "BE".to_string(),
133        json!({
134            "name": "Belgium",
135            "continent": "Europe",
136            "region": "EU",
137            "in_eu": true,
138            "in_schengen": true,
139        }),
140    );
141    countries.insert(
142        "CH".to_string(),
143        json!({
144            "name": "Switzerland",
145            "continent": "Europe",
146            "region": "EU",
147            "in_eu": false,
148            "in_schengen": true,
149        }),
150    );
151
152    // North America
153    countries.insert(
154        "US".to_string(),
155        json!({
156            "name": "United States",
157            "continent": "North America",
158            "region": "Americas",
159            "in_eu": false,
160            "in_schengen": false,
161        }),
162    );
163    countries.insert(
164        "CA".to_string(),
165        json!({
166            "name": "Canada",
167            "continent": "North America",
168            "region": "Americas",
169            "in_eu": false,
170            "in_schengen": false,
171        }),
172    );
173    countries.insert(
174        "MX".to_string(),
175        json!({
176            "name": "Mexico",
177            "continent": "North America",
178            "region": "Americas",
179            "in_eu": false,
180            "in_schengen": false,
181        }),
182    );
183
184    // South America
185    countries.insert(
186        "BR".to_string(),
187        json!({
188            "name": "Brazil",
189            "continent": "South America",
190            "region": "Americas",
191            "in_eu": false,
192            "in_schengen": false,
193        }),
194    );
195    countries.insert(
196        "AR".to_string(),
197        json!({
198            "name": "Argentina",
199            "continent": "South America",
200            "region": "Americas",
201            "in_eu": false,
202            "in_schengen": false,
203        }),
204    );
205
206    // Asia
207    countries.insert(
208        "CN".to_string(),
209        json!({
210            "name": "China",
211            "continent": "Asia",
212            "region": "Asia",
213            "in_eu": false,
214            "in_schengen": false,
215        }),
216    );
217    countries.insert(
218        "JP".to_string(),
219        json!({
220            "name": "Japan",
221            "continent": "Asia",
222            "region": "Asia",
223            "in_eu": false,
224            "in_schengen": false,
225        }),
226    );
227    countries.insert(
228        "IN".to_string(),
229        json!({
230            "name": "India",
231            "continent": "Asia",
232            "region": "Asia",
233            "in_eu": false,
234            "in_schengen": false,
235        }),
236    );
237    countries.insert(
238        "SG".to_string(),
239        json!({
240            "name": "Singapore",
241            "continent": "Asia",
242            "region": "Asia",
243            "in_eu": false,
244            "in_schengen": false,
245        }),
246    );
247
248    // Africa
249    countries.insert(
250        "ZA".to_string(),
251        json!({
252            "name": "South Africa",
253            "continent": "Africa",
254            "region": "Africa",
255            "in_eu": false,
256            "in_schengen": false,
257        }),
258    );
259    countries.insert(
260        "EG".to_string(),
261        json!({
262            "name": "Egypt",
263            "continent": "Africa",
264            "region": "Africa",
265            "in_eu": false,
266            "in_schengen": false,
267        }),
268    );
269
270    // Oceania
271    countries.insert(
272        "AU".to_string(),
273        json!({
274            "name": "Australia",
275            "continent": "Oceania",
276            "region": "Oceania",
277            "in_eu": false,
278            "in_schengen": false,
279        }),
280    );
281    countries.insert(
282        "NZ".to_string(),
283        json!({
284            "name": "New Zealand",
285            "continent": "Oceania",
286            "region": "Oceania",
287            "in_eu": false,
288            "in_schengen": false,
289        }),
290    );
291
292    countries
293}
294
295/// Build currency code lookup table with symbols and decimal places.
296fn build_currencies_lookup() -> HashMap<String, Value> {
297    let mut currencies = HashMap::new();
298
299    currencies.insert(
300        "USD".to_string(),
301        json!({
302            "name": "US Dollar",
303            "symbol": "$",
304            "decimal_places": 2,
305        }),
306    );
307    currencies.insert(
308        "EUR".to_string(),
309        json!({
310            "name": "Euro",
311            "symbol": "€",
312            "decimal_places": 2,
313        }),
314    );
315    currencies.insert(
316        "GBP".to_string(),
317        json!({
318            "name": "British Pound",
319            "symbol": "£",
320            "decimal_places": 2,
321        }),
322    );
323    currencies.insert(
324        "JPY".to_string(),
325        json!({
326            "name": "Japanese Yen",
327            "symbol": "¥",
328            "decimal_places": 0,
329        }),
330    );
331    currencies.insert(
332        "CHF".to_string(),
333        json!({
334            "name": "Swiss Franc",
335            "symbol": "₣",
336            "decimal_places": 2,
337        }),
338    );
339    currencies.insert(
340        "CAD".to_string(),
341        json!({
342            "name": "Canadian Dollar",
343            "symbol": "C$",
344            "decimal_places": 2,
345        }),
346    );
347    currencies.insert(
348        "AUD".to_string(),
349        json!({
350            "name": "Australian Dollar",
351            "symbol": "A$",
352            "decimal_places": 2,
353        }),
354    );
355    currencies.insert(
356        "CNY".to_string(),
357        json!({
358            "name": "Chinese Yuan",
359            "symbol": "¥",
360            "decimal_places": 2,
361        }),
362    );
363    currencies.insert(
364        "INR".to_string(),
365        json!({
366            "name": "Indian Rupee",
367            "symbol": "₹",
368            "decimal_places": 2,
369        }),
370    );
371    currencies.insert(
372        "MXN".to_string(),
373        json!({
374            "name": "Mexican Peso",
375            "symbol": "$",
376            "decimal_places": 2,
377        }),
378    );
379
380    currencies
381}
382
383/// Build timezone lookup table with UTC offsets and DST info.
384fn build_timezones_lookup() -> HashMap<String, Value> {
385    let mut timezones = HashMap::new();
386
387    // UTC
388    timezones.insert(
389        "UTC".to_string(),
390        json!({
391            "offset_minutes": 0,
392            "has_dst": false,
393            "region": "UTC",
394        }),
395    );
396
397    // Europe
398    timezones.insert(
399        "GMT".to_string(),
400        json!({
401            "offset_minutes": 0,
402            "has_dst": false,
403            "region": "Europe",
404        }),
405    );
406    timezones.insert(
407        "CET".to_string(),
408        json!({
409            "offset_minutes": 60,
410            "has_dst": true,
411            "region": "Europe",
412        }),
413    );
414    timezones.insert(
415        "CEST".to_string(),
416        json!({
417            "offset_minutes": 120,
418            "has_dst": false,
419            "region": "Europe",
420        }),
421    );
422
423    // North America
424    timezones.insert(
425        "EST".to_string(),
426        json!({
427            "offset_minutes": -300,
428            "has_dst": true,
429            "region": "Americas",
430        }),
431    );
432    timezones.insert(
433        "EDT".to_string(),
434        json!({
435            "offset_minutes": -240,
436            "has_dst": false,
437            "region": "Americas",
438        }),
439    );
440    timezones.insert(
441        "CST".to_string(),
442        json!({
443            "offset_minutes": -360,
444            "has_dst": true,
445            "region": "Americas",
446        }),
447    );
448    timezones.insert(
449        "CDT".to_string(),
450        json!({
451            "offset_minutes": -300,
452            "has_dst": false,
453            "region": "Americas",
454        }),
455    );
456    timezones.insert(
457        "MST".to_string(),
458        json!({
459            "offset_minutes": -420,
460            "has_dst": true,
461            "region": "Americas",
462        }),
463    );
464    timezones.insert(
465        "MDT".to_string(),
466        json!({
467            "offset_minutes": -360,
468            "has_dst": false,
469            "region": "Americas",
470        }),
471    );
472    timezones.insert(
473        "PST".to_string(),
474        json!({
475            "offset_minutes": -480,
476            "has_dst": true,
477            "region": "Americas",
478        }),
479    );
480    timezones.insert(
481        "PDT".to_string(),
482        json!({
483            "offset_minutes": -420,
484            "has_dst": false,
485            "region": "Americas",
486        }),
487    );
488
489    // Asia
490    timezones.insert(
491        "JST".to_string(),
492        json!({
493            "offset_minutes": 540,
494            "has_dst": false,
495            "region": "Asia",
496        }),
497    );
498    timezones.insert(
499        "IST".to_string(),
500        json!({
501            "offset_minutes": 330,
502            "has_dst": false,
503            "region": "Asia",
504        }),
505    );
506    timezones.insert(
507        "SGT".to_string(),
508        json!({
509            "offset_minutes": 480,
510            "has_dst": false,
511            "region": "Asia",
512        }),
513    );
514    timezones.insert(
515        "AEST".to_string(),
516        json!({
517            "offset_minutes": 600,
518            "has_dst": false,
519            "region": "Oceania",
520        }),
521    );
522
523    timezones
524}
525
526/// Build language code lookup table with language families.
527fn build_languages_lookup() -> HashMap<String, Value> {
528    let mut languages = HashMap::new();
529
530    // Indo-European
531    languages.insert(
532        "EN".to_string(),
533        json!({
534            "name": "English",
535            "family": "Indo-European",
536            "script": "Latin",
537        }),
538    );
539    languages.insert(
540        "FR".to_string(),
541        json!({
542            "name": "French",
543            "family": "Indo-European",
544            "script": "Latin",
545        }),
546    );
547    languages.insert(
548        "DE".to_string(),
549        json!({
550            "name": "German",
551            "family": "Indo-European",
552            "script": "Latin",
553        }),
554    );
555    languages.insert(
556        "ES".to_string(),
557        json!({
558            "name": "Spanish",
559            "family": "Indo-European",
560            "script": "Latin",
561        }),
562    );
563    languages.insert(
564        "IT".to_string(),
565        json!({
566            "name": "Italian",
567            "family": "Indo-European",
568            "script": "Latin",
569        }),
570    );
571    languages.insert(
572        "PT".to_string(),
573        json!({
574            "name": "Portuguese",
575            "family": "Indo-European",
576            "script": "Latin",
577        }),
578    );
579    languages.insert(
580        "RU".to_string(),
581        json!({
582            "name": "Russian",
583            "family": "Indo-European",
584            "script": "Cyrillic",
585        }),
586    );
587
588    // Sino-Tibetan
589    languages.insert(
590        "ZH".to_string(),
591        json!({
592            "name": "Chinese",
593            "family": "Sino-Tibetan",
594            "script": "Han",
595        }),
596    );
597
598    // Japonic
599    languages.insert(
600        "JA".to_string(),
601        json!({
602            "name": "Japanese",
603            "family": "Japonic",
604            "script": "Japanese",
605        }),
606    );
607
608    // Koreanic
609    languages.insert(
610        "KO".to_string(),
611        json!({
612            "name": "Korean",
613            "family": "Koreanic",
614            "script": "Hangul",
615        }),
616    );
617
618    // Austroasiatic
619    languages.insert(
620        "VI".to_string(),
621        json!({
622            "name": "Vietnamese",
623            "family": "Austroasiatic",
624            "script": "Latin",
625        }),
626    );
627
628    languages
629}
630
631#[cfg(test)]
632mod tests {
633    use super::*;
634
635    #[test]
636    fn test_build_lookup_data() {
637        let data = build_lookup_data();
638
639        assert!(data.get("countries").is_some());
640        assert!(data.get("currencies").is_some());
641        assert!(data.get("timezones").is_some());
642        assert!(data.get("languages").is_some());
643    }
644
645    #[test]
646    fn test_countries_have_required_fields() {
647        let countries = build_countries_lookup();
648
649        for (code, data) in countries {
650            assert!(data.get("name").is_some(), "Country {code} missing name");
651            assert!(data.get("continent").is_some(), "Country {code} missing continent");
652            assert!(data.get("in_eu").is_some(), "Country {code} missing in_eu");
653            assert!(data.get("in_schengen").is_some(), "Country {code} missing in_schengen");
654        }
655    }
656
657    #[test]
658    fn test_currencies_have_required_fields() {
659        let currencies = build_currencies_lookup();
660
661        for (code, data) in currencies {
662            assert!(data.get("name").is_some(), "Currency {code} missing name");
663            assert!(data.get("symbol").is_some(), "Currency {code} missing symbol");
664            assert!(data.get("decimal_places").is_some(), "Currency {code} missing decimal_places");
665        }
666    }
667
668    #[test]
669    fn test_timezones_have_required_fields() {
670        let timezones = build_timezones_lookup();
671
672        for (code, data) in timezones {
673            assert!(data.get("offset_minutes").is_some(), "Timezone {code} missing offset_minutes");
674            assert!(data.get("has_dst").is_some(), "Timezone {code} missing has_dst");
675        }
676    }
677
678    #[test]
679    fn test_eu_member_states() {
680        let countries = build_countries_lookup();
681
682        // Check some known EU members
683        assert!(countries["FR"]["in_eu"].as_bool().unwrap());
684        assert!(countries["DE"]["in_eu"].as_bool().unwrap());
685        assert!(countries["IT"]["in_eu"].as_bool().unwrap());
686
687        // Check non-EU
688        assert!(!countries["US"]["in_eu"].as_bool().unwrap());
689        assert!(!countries["GB"]["in_eu"].as_bool().unwrap());
690    }
691
692    #[test]
693    fn test_schengen_members() {
694        let countries = build_countries_lookup();
695
696        // Check some known Schengen members
697        assert!(countries["FR"]["in_schengen"].as_bool().unwrap());
698        assert!(countries["DE"]["in_schengen"].as_bool().unwrap());
699        assert!(countries["CH"]["in_schengen"].as_bool().unwrap());
700
701        // Check non-Schengen
702        assert!(!countries["US"]["in_schengen"].as_bool().unwrap());
703        assert!(!countries["GB"]["in_schengen"].as_bool().unwrap());
704    }
705}