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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
//! **H**ttp **E**rror **Man**ual
//!
//! `heman` is a [CLI](https://en.wikipedia.org/wiki/Command-line_interface)
//! for the [http-status-codes2](https://crates.io/crates/http-status-codes2) library which models
//! [IANA's](https://www.iana.org/)
//! [HTTP Status Code Registry](https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml).
//!
//! It allows you to quickly search for a HTTP status code by its code or description,
//! optionally providing the references and links to the
//! [RFC](https://en.wikipedia.org/wiki/Request_for_Comments)s where the status code was specified.
//!
//! # Usage
//! Use `heman help` for instructions how to use the CLI.
//! ## Examples
//!
//! Search by status code to find what that code means:
//!  ```bash
//!  $ heman code 401
//!  401 Unauthorized
//!  ```
//!
//! Search by a substring of the status code description:
//!  ```bash
//!  $ heman search redirect
//!  307 Temporary Redirect
//!  308 Permanent Redirect
//!  ```
//! ### Unofficial Code Registry
//! The [http-status-codes2](https://crates.io/crates/http-status-codes2) library also contains an unofficial code registry
//! of a handful of arbitrarily selected status codes which have been proposed
//! or used with some degree of popularity.
//! This unofficial registry can be optionally included in the query using the `--unofficial` flag:
//!  ```bash
//!  $ heman --unofficial search pot
//!  418 I'm a teapot
//!  ```
//!
//! ### Reference, Link
//! To see the reference to the HTTP status code, use the `--reference` flag. For a link to that reference, use `--link`:
//!  ```bash
//!  $ heman --reference code 403
//!  403 Forbidden, [RFC9110, Section 15.5.4]
//!  ```
//!  ```bash
//!  $ heman --link search timeout
//!  408 Request Timeout, https://www.rfc-editor.org/rfc/rfc9110.html#section-15.5.9
//!  504 Gateway Timeout, https://www.rfc-editor.org/rfc/rfc9110.html#section-15.6.5
//!  ```
//!  ```bash
//!  $ heman -rl code 300
//!  300 Multiple Choices, [RFC9110, Section 15.4.1], https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4.1
//!  ```
//!
//! ### Environment Variables
//!
//! Instead of the CLI flags you can set these environment variables:
//!
//! | flag       | variable                            |
//! | ---------- | ----------------------------------- |
//! | reference  | `HEMAN_OUTPUT_REFERENCE`            |
//! | link       | `HEMAN_OUTPUT_LINK`                 |
//! | unofficial | `HEMAN_INCLUDE_UNOFFICIAL_REGISTRY` |
//!
//! **NOTE:** The heman CLI only checks if one of these environment variables is set. The value does not matter.
//!
//! ```bash
//! $ HEMAN_INCLUDE_UNOFFICIAL_REGISTRY=1
//! $ HEMAN_OUTPUT_REFERENCE=1
//! $ heman search pot
//! 418 I'm a teapot, [RFC2324, Section 2.3.2]
//! ```

#![deny(missing_docs)]
pub mod cli;
mod error;

use clap::ArgMatches;
use error::Error;
use http_status_codes2::{
    find_by_code, find_by_substring,
    status_code_registry::{CODE_REGISTRY, UNOFFICIAL_CODE_REGISTRY},
};
use std::{
    env,
    io::{stdout, Write},
};

const REFERENCE: &str = "HEMAN_OUTPUT_REFERENCE";
const LINK: &str = "HEMAN_OUTPUT_LINK";
const UNOFFICIAL: &str = "HEMAN_INCLUDE_UNOFFICIAL_REGISTRY";

/// CLI logic. Translates a command from the CLI to the corresponding API calls.
pub fn translate_to_api_calls(matches: ArgMatches) -> Result<(), Error> {
    let want_reference = matches.get_flag("reference") || env::var(REFERENCE).is_ok();
    let want_link = matches.get_flag("link") || env::var(LINK).is_ok();
    let want_unofficial = matches.get_flag("unofficial") || env::var(UNOFFICIAL).is_ok();
    match matches.subcommand() {
        Some(("code", sub_matches)) => {
            let code: usize = *sub_matches.get_one("CODE").expect("required");
            if want_unofficial {
                let unofficial_result = find_by_code(code, &UNOFFICIAL_CODE_REGISTRY);
                if unofficial_result.is_some() {
                    return output(unofficial_result, want_reference, want_link);
                }
            }
            output(
                find_by_code(code, &CODE_REGISTRY),
                want_reference,
                want_link,
            )
        }
        Some(("search", sub_matches)) => {
            let substring = sub_matches.get_one::<String>("STRING").expect("required");
            if want_unofficial {
                let unofficial_result = find_by_substring(substring, &UNOFFICIAL_CODE_REGISTRY);
                let official_result = find_by_substring(substring, &CODE_REGISTRY);
                output_iter(
                    unofficial_result.chain(official_result),
                    want_reference,
                    want_link,
                )
            } else {
                output_iter(
                    find_by_substring(substring, &CODE_REGISTRY),
                    want_reference,
                    want_link,
                )
            }
        }
        _ => unreachable!(),
    }
}

fn output(
    maybe_item: Option<(usize, &str, &str, &str)>,
    want_reference: bool,
    want_link: bool,
) -> Result<(), Error> {
    let item = maybe_item.ok_or(Error::Unassigned)?;
    let mut lock = stdout().lock();
    write!(lock, "{} {}", item.0, item.1).unwrap();
    if want_reference {
        write!(lock, ", {}", item.2).unwrap();
    }
    if want_link {
        write!(lock, ", {}", item.3).unwrap();
    }
    write!(lock, "\n").unwrap();
    Ok(())
}

fn output_iter(
    it: impl Iterator<Item = &'static (usize, &'static str, &'static str, &'static str)>,
    want_reference: bool,
    want_link: bool,
) -> Result<(), Error> {
    let mut found_some = false;
    for item in it {
        if !found_some {
            found_some = true;
        }
        output(Some(*item), want_reference, want_link)?;
    }
    if !found_some {
        return Err(Error::NotFound);
    }
    Ok(())
}