summaryrefslogtreecommitdiffstats
path: root/src/bin/cepis.rs
blob: 252cb90c613d7f3fadd3fad9324a13a9100d58f4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
//! Minimal CLI: `cepis 01001000`.
//!
//! It exists for one concrete case — quickly confirming a local instance
//! answers before pointing code at it:
//!
//! ```sh
//! cepis --base-url http://localhost:8080 01001000
//! ```
//!
//! A single CEP goes through the unit lookup, which carries `geo`; several go
//! through the batch, which is one request instead of N and has no `geo`.
//!
//! It is not the library's main interface; for that, use the `cepis` crate.

use std::process::ExitCode;
use std::time::Duration;

use cepis::{Client, DEFAULT_URL};
use serde_json::Value;

const USAGE: &str = "\
usage: cepis [--base-url URL] [--timeout SECONDS] CEP [CEP ...]

Look CEPs up against the cep.is API.

  --base-url URL      API URL (default: https://cep.is) — point it at a local instance
  --timeout SECONDS   per-socket timeout (default: 10)
  -h, --help          show this message";

fn main() -> ExitCode {
    let mut base_url = DEFAULT_URL.to_owned();
    let mut timeout = Duration::from_secs(10);
    let mut ceps: Vec<String> = Vec::new();
    let mut args = std::env::args().skip(1);

    while let Some(arg) = args.next() {
        match arg.as_str() {
            "-h" | "--help" => {
                println!("{USAGE}");
                return ExitCode::SUCCESS;
            }
            "--base-url" => match args.next() {
                Some(value) => base_url = value,
                None => return fail("--base-url needs a URL"),
            },
            "--timeout" => match args.next().map(|v| v.parse::<f64>()) {
                Some(Ok(seconds)) if seconds > 0.0 => timeout = Duration::from_secs_f64(seconds),
                _ => return fail("--timeout needs a positive number of seconds"),
            },
            other if other.starts_with('-') => return fail(&format!("unknown option {other}")),
            other => ceps.push(other.to_owned()),
        }
    }

    if ceps.is_empty() {
        eprintln!("{USAGE}");
        return ExitCode::FAILURE;
    }

    let client = Client::builder()
        .base_url(base_url)
        .timeout(timeout)
        .build();

    let addresses = if ceps.len() == 1 {
        match client.lookup(&ceps[0]) {
            Ok(address) => vec![Some(address)],
            Err(err) => return fail(&err.to_string()),
        }
    } else {
        match client.lookup_many(&ceps) {
            Ok(found) => found,
            Err(err) => return fail(&err.to_string()),
        }
    };

    let output: Vec<Value> = addresses
        .into_iter()
        .map(|a| a.map(|a| a.raw).unwrap_or(Value::Null))
        .collect();

    let rendered = if output.len() == 1 {
        serde_json::to_string_pretty(&output[0])
    } else {
        serde_json::to_string_pretty(&output)
    };

    match rendered {
        Ok(text) => {
            println!("{text}");
            ExitCode::SUCCESS
        }
        Err(err) => fail(&err.to_string()),
    }
}

fn fail(message: &str) -> ExitCode {
    eprintln!("error: {message}");
    ExitCode::FAILURE
}