Skip to main content

remilia_read_api/
main.rs

1use octra_sqlite::{Client, Database, QueryResult, Value};
2use serde_json::{Map, json};
3use std::{
4    env,
5    error::Error,
6    io::{BufRead, BufReader, Write},
7    net::{TcpListener, TcpStream},
8};
9
10type AppResult<T> = Result<T, Box<dyn Error>>;
11
12fn main() -> AppResult<()> {
13    let addr = env::var("OCTRA_SQLITE_API_ADDR").unwrap_or_else(|_| "127.0.0.1:8787".to_string());
14    let database_name = env::var("OCTRA_SQLITE_DATABASE").unwrap_or_else(|_| "remilia".to_string());
15    let client = Client::from_default_config()?;
16    let database = client.database(database_name.clone())?;
17    let listener = TcpListener::bind(&addr)?;
18
19    eprintln!("listening on http://{addr}");
20    eprintln!("try: curl http://{addr}/collections/milady");
21
22    for stream in listener.incoming() {
23        match stream {
24            Ok(stream) => {
25                if let Err(error) = handle_connection(stream, &database, &database_name) {
26                    eprintln!("request error: {error}");
27                }
28            }
29            Err(error) => eprintln!("accept error: {error}"),
30        }
31    }
32
33    Ok(())
34}
35
36fn handle_connection(
37    mut stream: TcpStream,
38    database: &Database,
39    database_name: &str,
40) -> AppResult<()> {
41    let mut reader = BufReader::new(stream.try_clone()?);
42    let mut request_line = String::new();
43    reader.read_line(&mut request_line)?;
44
45    let mut parts = request_line.split_whitespace();
46    let method = parts.next().unwrap_or_default();
47    let path = parts.next().unwrap_or_default();
48
49    if method != "GET" {
50        return write_json(
51            &mut stream,
52            "405 Method Not Allowed",
53            &json!({"error":"method_not_allowed"}),
54        );
55    }
56
57    if path == "/" || path == "/health" {
58        return write_json(
59            &mut stream,
60            "200 OK",
61            &json!({"ok":true,"database":database_name}),
62        );
63    }
64
65    let Some(slug) = collection_slug(path) else {
66        return write_json(
67            &mut stream,
68            "404 Not Found",
69            &json!({"error":"not_found","routes":["GET /collections/<opensea_slug>"]}),
70        );
71    };
72
73    let result = query_collection(database, &slug)?;
74    write_json(
75        &mut stream,
76        "200 OK",
77        &json!({
78            "database": database_name,
79            "collection": slug,
80            "row_count": result.row_count,
81            "rows": rows_as_objects(&result),
82        }),
83    )
84}
85
86fn query_collection(database: &Database, slug: &str) -> AppResult<QueryResult> {
87    let sql = format!(
88        "select name, opensea_slug, chain, relationship, launched_month, date_precision \
89         from collection where opensea_slug = {} limit 1;",
90        sql_quote(slug)
91    );
92    Ok(database.query(&sql)?)
93}
94
95fn collection_slug(path: &str) -> Option<String> {
96    let path = path.split('?').next().unwrap_or(path);
97    let slug = path.strip_prefix("/collections/")?;
98    if slug.is_empty() {
99        None
100    } else {
101        Some(slug.to_string())
102    }
103}
104
105fn rows_as_objects(result: &QueryResult) -> Vec<Value> {
106    result
107        .rows
108        .iter()
109        .map(|row| {
110            let mut object = Map::new();
111            for (column, value) in result.columns.iter().zip(row) {
112                object.insert(column.clone(), value.clone());
113            }
114            Value::Object(object)
115        })
116        .collect()
117}
118
119fn sql_quote(value: &str) -> String {
120    format!("'{}'", value.replace('\'', "''"))
121}
122
123fn write_json(stream: &mut TcpStream, status: &str, value: &Value) -> AppResult<()> {
124    let body = serde_json::to_vec_pretty(value)?;
125    write!(
126        stream,
127        "HTTP/1.1 {status}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n",
128        body.len()
129    )?;
130    stream.write_all(&body)?;
131    Ok(())
132}