Skip to main content

pylon_http/
lib.rs

1use std::fmt;
2
3// ---------------------------------------------------------------------------
4// HttpMethod — platform-agnostic HTTP verb
5// ---------------------------------------------------------------------------
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum HttpMethod {
9    Get,
10    Post,
11    Put,
12    Patch,
13    Delete,
14    Options,
15    Head,
16}
17
18impl HttpMethod {
19    /// Parse an HTTP method string. Returns `None` for unrecognized methods.
20    pub fn try_parse(s: &str) -> Option<Self> {
21        match s {
22            "GET" | "get" => Some(Self::Get),
23            "POST" | "post" => Some(Self::Post),
24            "PUT" | "put" => Some(Self::Put),
25            "PATCH" | "patch" => Some(Self::Patch),
26            "DELETE" | "delete" => Some(Self::Delete),
27            "OPTIONS" | "options" => Some(Self::Options),
28            "HEAD" | "head" => Some(Self::Head),
29            _ => None,
30        }
31    }
32
33    /// Parse an HTTP method string, falling back to `Get` for unrecognized methods.
34    /// Prefer `try_parse` to detect malformed inputs; this remains for compatibility.
35    #[allow(clippy::should_implement_trait)]
36    pub fn from_str(s: &str) -> Self {
37        Self::try_parse(s).unwrap_or(Self::Get)
38    }
39
40    pub fn as_str(&self) -> &'static str {
41        match self {
42            Self::Get => "GET",
43            Self::Post => "POST",
44            Self::Put => "PUT",
45            Self::Patch => "PATCH",
46            Self::Delete => "DELETE",
47            Self::Options => "OPTIONS",
48            Self::Head => "HEAD",
49        }
50    }
51
52    /// True for methods that never have a request body.
53    pub fn is_bodyless(&self) -> bool {
54        matches!(self, Self::Get | Self::Head | Self::Options | Self::Delete)
55    }
56}
57
58impl fmt::Display for HttpMethod {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        f.write_str(self.as_str())
61    }
62}
63
64// ---------------------------------------------------------------------------
65// DataError — platform-agnostic error from data operations
66// ---------------------------------------------------------------------------
67
68#[derive(Debug, Clone)]
69pub struct DataError {
70    pub code: String,
71    pub message: String,
72}
73
74impl fmt::Display for DataError {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        write!(f, "[{}] {}", self.code, self.message)
77    }
78}
79
80impl std::error::Error for DataError {}
81
82// ---------------------------------------------------------------------------
83// DataStore — platform-agnostic data access trait
84// ---------------------------------------------------------------------------
85
86/// Platform-agnostic data store trait.
87///
88/// Implemented by `Runtime` (SQLite, self-hosted) and `D1DataStore` (Workers).
89/// All methods are synchronous to keep the trait `Send + Sync` and simple;
90/// Workers adapters can use `block_on` or similar bridging.
91pub trait DataStore: Send + Sync {
92    fn manifest(&self) -> &pylon_kernel::AppManifest;
93
94    fn insert(&self, entity: &str, data: &serde_json::Value) -> Result<String, DataError>;
95
96    fn get_by_id(&self, entity: &str, id: &str) -> Result<Option<serde_json::Value>, DataError>;
97
98    fn list(&self, entity: &str) -> Result<Vec<serde_json::Value>, DataError>;
99
100    fn list_after(
101        &self,
102        entity: &str,
103        after: Option<&str>,
104        limit: usize,
105    ) -> Result<Vec<serde_json::Value>, DataError>;
106
107    fn update(&self, entity: &str, id: &str, data: &serde_json::Value) -> Result<bool, DataError>;
108
109    fn delete(&self, entity: &str, id: &str) -> Result<bool, DataError>;
110
111    fn lookup(
112        &self,
113        entity: &str,
114        field: &str,
115        value: &str,
116    ) -> Result<Option<serde_json::Value>, DataError>;
117
118    fn link(
119        &self,
120        entity: &str,
121        id: &str,
122        relation: &str,
123        target_id: &str,
124    ) -> Result<bool, DataError>;
125
126    fn unlink(&self, entity: &str, id: &str, relation: &str) -> Result<bool, DataError>;
127
128    fn query_filtered(
129        &self,
130        entity: &str,
131        filter: &serde_json::Value,
132    ) -> Result<Vec<serde_json::Value>, DataError>;
133
134    fn query_graph(&self, query: &serde_json::Value) -> Result<serde_json::Value, DataError>;
135
136    /// Run an aggregation query.
137    ///
138    /// Spec shape (same vocabulary in the HTTP body):
139    /// ```json
140    /// {
141    ///   "count": "*",
142    ///   "sum": ["amount"],
143    ///   "avg": ["price"],
144    ///   "min": ["createdAt"],
145    ///   "max": ["createdAt"],
146    ///   "groupBy": ["status"],
147    ///   "where": { ...standard filter... }
148    /// }
149    /// ```
150    /// Returns `{rows: [{count, sum_amount, ...}]}`.
151    /// Default implementation returns `NOT_SUPPORTED`; Runtime overrides it.
152    fn aggregate(
153        &self,
154        _entity: &str,
155        _spec: &serde_json::Value,
156    ) -> Result<serde_json::Value, DataError> {
157        Err(DataError {
158            code: "NOT_SUPPORTED".into(),
159            message: "aggregate() is not implemented by this backend".into(),
160        })
161    }
162
163    /// Execute transactional operations. Each element is a JSON object with
164    /// `op` ("insert"/"update"/"delete"), `entity`, and optionally `id`/`data`.
165    ///
166    /// Returns per-operation results. The implementation decides whether to
167    /// use real SQL transactions (Runtime) or sequential execution (D1).
168    fn transact(
169        &self,
170        ops: &[serde_json::Value],
171    ) -> Result<(bool, Vec<serde_json::Value>), DataError>;
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn http_method_roundtrip() {
180        assert_eq!(HttpMethod::from_str("GET"), HttpMethod::Get);
181        assert_eq!(HttpMethod::from_str("post"), HttpMethod::Post);
182        assert_eq!(HttpMethod::from_str("DELETE"), HttpMethod::Delete);
183        assert_eq!(HttpMethod::Get.as_str(), "GET");
184    }
185
186    #[test]
187    fn data_error_display() {
188        let e = DataError {
189            code: "TEST".into(),
190            message: "fail".into(),
191        };
192        assert_eq!(format!("{e}"), "[TEST] fail");
193    }
194}