Skip to main content

rsynko_x/
laws.rs

1//! Law scenarios for reading a tweet, stated once over the capabilities.
2//!
3//! Everything a tweet source decides before it asks anything is decided here: which addresses
4//! name a tweet, what asking about one is, and which files a way of taking accepts. A scenario
5//! authors its own addresses, so it constrains any interpreter of the sorts.
6
7use crate::*;
8use alux_ext::ext;
9use anyhow::{Result, bail};
10
11/// Names one tweet every scenario reads addresses of.
12const LAW_STATUS: &str = "1234567890123456789";
13
14/// Authors the tweet-reading laws.
15#[ext(name = XLaws)]
16pub impl<This> This
17where
18    This: XRequestAlg + XRequestViewAlg,
19{
20    /// Checks that an address, a request, and a way of taking each mean one thing.
21    ///
22    /// The laws checked are:
23    ///
24    /// 1. every address of one tweet names that tweet, however it was written;
25    /// 2. an address naming no tweet names none, whatever else it looks like;
26    /// 3. asking about one tweet is a function of which tweet it is, and always carries a token;
27    /// 4. every way of taking is named by its own word and says what it does;
28    /// 5. taking everything accepts every kind, and each other way accepts its own and no other;
29    /// 6. every kind the answer names reads back as itself.
30    ///
31    /// # Errors
32    ///
33    /// Returns the first violated law.
34    fn x_laws(&self) -> Result<()> {
35        self.check_address_laws()?;
36        self.check_request_laws()?;
37        check_taking_laws()
38    }
39
40    /// Checks which addresses name a tweet.
41    ///
42    /// # Errors
43    ///
44    /// Returns the first violated law.
45    fn check_address_laws(&self) -> Result<()> {
46        for host in X_HOSTS {
47            for shape in [
48                format!("https://{host}/somebody/status/{LAW_STATUS}"),
49                format!("http://{host}/somebody/status/{LAW_STATUS}"),
50                format!("https://{host}/somebody/statuses/{LAW_STATUS}"),
51                format!("https://{host}/somebody/status/{LAW_STATUS}?s=20&t=abc"),
52                format!("https://{host}/somebody/status/{LAW_STATUS}/photo/1"),
53                format!("  https://{host}/somebody/status/{LAW_STATUS}  "),
54            ] {
55                if status_id(&shape).as_deref() != Some(LAW_STATUS) {
56                    bail!("{shape} does not name the tweet it addresses");
57                }
58            }
59        }
60        for shape in [
61            "https://example.test/somebody/status/1234567890123456789",
62            "https://x.com/somebody",
63            "https://x.com/somebody/status/",
64            "https://x.com/somebody/status/not-a-number",
65            "https://x.com.example.test/somebody/status/1234567890123456789",
66            "/home/dev/x.com/somebody/status/1234567890123456789",
67            "https://www.youtube.com/watch?v=VIDEO_ID",
68        ] {
69            if let Some(named) = status_id(shape) {
70                bail!("{shape} names the tweet {named}, and addresses no tweet");
71            }
72        }
73        Ok(())
74    }
75
76    /// Checks what asking about one tweet is.
77    ///
78    /// # Errors
79    ///
80    /// Returns the first violated law.
81    fn check_request_laws(&self) -> Result<()> {
82        let asked = self.status_request(LAW_STATUS);
83        let asked_again = self.status_request(LAW_STATUS);
84        let address = self.request_address(&asked).to_owned();
85        if address != self.request_address(&asked_again) {
86            bail!("asking twice about one tweet asks two different things");
87        }
88        if !address.starts_with(READING_ADDRESS) {
89            bail!("a tweet is asked about somewhere other than where tweets are read");
90        }
91        if !address.contains(&format!("id={LAW_STATUS}")) {
92            bail!("asking about a tweet does not name which tweet");
93        }
94        // The address answers with nothing at all unless it is asked with a token.
95        let stated = reading_token(LAW_STATUS);
96        if stated.is_empty() || !address.contains(&format!("token={stated}")) {
97            bail!("asking about a tweet carries no token");
98        }
99        if reading_token("1") == reading_token("2") {
100            bail!("two tweets are asked about with one token");
101        }
102        Ok(())
103    }
104}
105
106/// Checks which files each way of taking accepts.
107///
108/// # Errors
109///
110/// Returns the first violated law.
111fn check_taking_laws() -> Result<()> {
112    for (place, way) in take::ALL.iter().copied().enumerate() {
113        let named = take::to(way);
114        if take::ALL[..place].iter().any(|earlier| take::to(*earlier) == named) {
115            bail!("two ways of taking are named {named}");
116        }
117        if take::from(named) != Some(way) {
118            bail!("{named} is not read back as itself");
119        }
120        if way.summary().is_empty() {
121            bail!("{named} does not say what it does");
122        }
123    }
124    if take::from("nothing anybody would call a way of taking").is_some() {
125        bail!("a word naming no way of taking names one");
126    }
127
128    for kind in attachment_kind::ALL.iter().copied() {
129        if !Take::Everything.accepts(kind) {
130            bail!("taking everything leaves {} behind", attachment_kind::to(kind));
131        }
132        // Every kind is taken by exactly one of the ways that are not everything, so no file a
133        // tweet carries is unreachable and none is offered twice.
134        let ways = [Take::Videos, Take::Images].into_iter().filter(|way| way.accepts(kind)).count();
135        if ways != 1 {
136            bail!("{} is taken by {ways} ways rather than one", attachment_kind::to(kind));
137        }
138        if attachment_kind::from(attachment_kind::to(kind)) != Some(kind) {
139            bail!("{} is not read back as itself", attachment_kind::to(kind));
140        }
141    }
142    Ok(())
143}