1use crate::{api, error::Result, storage};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::collections::BTreeMap;
9
10#[derive(Clone, Copy, Debug, Serialize)]
11#[serde(rename_all = "snake_case")]
12pub enum Dataset {
13 Input,
14 Output,
15}
16
17#[derive(Debug, Deserialize)]
18#[serde(deny_unknown_fields)]
19pub struct Parameters {
20 pub from_version: String,
21 pub to_version: String,
22 pub scope: String,
23}
24
25#[derive(Debug, Deserialize)]
26#[serde(deny_unknown_fields)]
27pub struct Settings {
28 pub scope: String,
29 pub revision: u64,
30 pub values: BTreeMap<String, Value>,
31}
32
33#[derive(Serialize)]
34struct Operation<'a, T> {
35 operation: &'a str,
36 #[serde(flatten)]
37 request: &'a T,
38}
39
40#[derive(Serialize)]
41struct Arguments<'a, T> {
42 dataset: Dataset,
43 kind: &'a str,
44 request: Operation<'a, T>,
45}
46
47fn call<T: Serialize, R: for<'de> Deserialize<'de>>(
48 dataset: Dataset,
49 kind: &str,
50 operation: &str,
51 request: &T,
52) -> Result<R> {
53 api::call(
54 "data.upgrade",
55 &Arguments {
56 dataset,
57 kind,
58 request: Operation { operation, request },
59 },
60 )
61}
62
63pub fn read_settings(dataset: Dataset) -> Result<Settings> {
64 call(dataset, "settings", "read", &serde_json::json!({}))
65}
66
67pub fn write_settings(values: BTreeMap<String, Value>) -> Result<()> {
68 let _: Value = call(
69 Dataset::Output,
70 "settings",
71 "write",
72 &serde_json::json!({"values": values}),
73 )?;
74 Ok(())
75}
76
77pub mod files {
78 use super::*;
79 pub fn read(
80 dataset: Dataset,
81 request: storage::files::ReadRequest,
82 ) -> Result<storage::files::ReadResponse> {
83 call(dataset, "files", "read", &request)
84 }
85 pub fn write(request: storage::files::WriteRequest) -> Result<storage::files::WriteResponse> {
86 call(Dataset::Output, "files", "write", &request)
87 }
88 pub fn delete(
89 request: storage::files::DeleteRequest,
90 ) -> Result<storage::files::DeleteResponse> {
91 call(Dataset::Output, "files", "delete", &request)
92 }
93 pub fn list(
94 dataset: Dataset,
95 request: storage::files::ListRequest,
96 ) -> Result<storage::files::ListResponse> {
97 call(dataset, "files", "list", &request)
98 }
99}
100
101pub mod kv {
102 use super::*;
103 pub fn get(
104 dataset: Dataset,
105 request: storage::kv::GetRequest,
106 ) -> Result<storage::kv::GetResponse> {
107 call(dataset, "kv", "get", &request)
108 }
109 pub fn put(request: storage::kv::PutRequest) -> Result<storage::kv::PutResponse> {
110 call(Dataset::Output, "kv", "put", &request)
111 }
112 pub fn delete(request: storage::kv::DeleteRequest) -> Result<storage::kv::DeleteResponse> {
113 call(Dataset::Output, "kv", "delete", &request)
114 }
115 pub fn list(
116 dataset: Dataset,
117 request: storage::kv::ListRequest,
118 ) -> Result<storage::kv::ListResponse> {
119 call(dataset, "kv", "list", &request)
120 }
121}
122
123pub mod sqlite {
124 use super::*;
125 pub fn query(
126 dataset: Dataset,
127 request: storage::sqlite::QueryRequest,
128 ) -> Result<storage::sqlite::QueryResponse> {
129 call(dataset, "sqlite", "query", &request)
130 }
131 pub fn exec(request: storage::sqlite::ExecRequest) -> Result<storage::sqlite::ExecResponse> {
132 call(Dataset::Output, "sqlite", "exec", &request)
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 fn round_trip<T: Serialize + for<'de> Deserialize<'de>>(value: Value) -> Value {
141 serde_json::to_value(serde_json::from_value::<T>(value).unwrap()).unwrap()
142 }
143
144 #[test]
145 fn shared_fixtures_match_author_storage_types() {
146 let fixtures: Value =
147 serde_json::from_str(include_str!("../testdata/data-upgrade-fixtures.json")).unwrap();
148 for fixture in fixtures["valid"].as_array().unwrap() {
149 let envelope = &fixture["request"];
150 let args = &envelope["arguments"];
151 let kind = args["kind"].as_str().unwrap();
152 let mut request = args["request"].clone();
153 let operation = request
154 .as_object_mut()
155 .unwrap()
156 .remove("operation")
157 .unwrap();
158 let operation = operation.as_str().unwrap();
159 let encoded = match (kind, operation) {
160 ("files", "read") => round_trip::<storage::files::ReadRequest>(request),
161 ("files", "write") => round_trip::<storage::files::WriteRequest>(request),
162 ("files", "delete") => round_trip::<storage::files::DeleteRequest>(request),
163 ("files", "list") => round_trip::<storage::files::ListRequest>(request),
164 ("kv", "get") => round_trip::<storage::kv::GetRequest>(request),
165 ("kv", "put") => round_trip::<storage::kv::PutRequest>(request),
166 ("kv", "delete") => round_trip::<storage::kv::DeleteRequest>(request),
167 ("kv", "list") => round_trip::<storage::kv::ListRequest>(request),
168 ("sqlite", "query") => round_trip::<storage::sqlite::QueryRequest>(request),
169 ("sqlite", "exec") => round_trip::<storage::sqlite::ExecRequest>(request),
170 ("settings", "read" | "write") => request,
171 _ => panic!("uncovered canonical operation"),
172 };
173 let dataset = match args["dataset"].as_str().unwrap() {
174 "input" => Dataset::Input,
175 "output" => Dataset::Output,
176 _ => panic!("invalid dataset"),
177 };
178 let actual = serde_json::to_value(Arguments {
179 dataset,
180 kind,
181 request: Operation {
182 operation,
183 request: &encoded,
184 },
185 })
186 .unwrap();
187 assert_eq!(&actual, args, "{}", fixture["name"]);
188 }
189 }
190
191 #[test]
192 fn upgrade_reuses_storage_requests_with_explicit_dataset() {
193 let request = storage::files::ReadRequest {
194 store_id: "notes".into(),
195 path: "note.txt".into(),
196 max_bytes: Some(4096),
197 };
198 let value = serde_json::to_value(Arguments {
199 dataset: Dataset::Input,
200 kind: "files",
201 request: Operation {
202 operation: "read",
203 request: &request,
204 },
205 })
206 .unwrap();
207 assert_eq!(
208 value,
209 serde_json::json!({"dataset":"input","kind":"files","request":{"operation":"read","store_id":"notes","path":"note.txt","max_bytes":4096}})
210 );
211 }
212}