+200
-7
Cargo.lock
+200
-7
Cargo.lock
···
1289
1289
]
1290
1290
1291
1291
[[package]]
1292
+
name = "derive_more"
1293
+
version = "2.0.1"
1294
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1295
+
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
1296
+
dependencies = [
1297
+
"derive_more-impl",
1298
+
]
1299
+
1300
+
[[package]]
1301
+
name = "derive_more-impl"
1302
+
version = "2.0.1"
1303
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1304
+
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
1305
+
dependencies = [
1306
+
"proc-macro2",
1307
+
"quote",
1308
+
"syn 2.0.103",
1309
+
"unicode-xid",
1310
+
]
1311
+
1312
+
[[package]]
1292
1313
name = "derive_utils"
1293
1314
version = "0.15.0"
1294
1315
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2283
2304
2284
2305
[[package]]
2285
2306
name = "hyper-util"
2286
-
version = "0.1.14"
2307
+
version = "0.1.16"
2287
2308
source = "registry+https://github.com/rust-lang/crates.io-index"
2288
-
checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
2309
+
checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e"
2289
2310
dependencies = [
2290
2311
"base64 0.22.1",
2291
2312
"bytes",
···
2299
2320
"libc",
2300
2321
"percent-encoding",
2301
2322
"pin-project-lite",
2302
-
"socket2 0.5.9",
2323
+
"socket2 0.6.0",
2303
2324
"system-configuration",
2304
2325
"tokio",
2305
2326
"tower-service",
···
3234
3255
"memchr",
3235
3256
"mime",
3236
3257
"spin",
3258
+
"tokio",
3237
3259
"version_check",
3238
3260
]
3239
3261
···
3729
3751
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
3730
3752
3731
3753
[[package]]
3754
+
name = "poem"
3755
+
version = "3.1.12"
3756
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3757
+
checksum = "9f977080932c87287147dca052951c3e2696f8759863f6b4e4c0c9ffe7a4cc8b"
3758
+
dependencies = [
3759
+
"bytes",
3760
+
"futures-util",
3761
+
"headers",
3762
+
"http",
3763
+
"http-body-util",
3764
+
"hyper",
3765
+
"hyper-util",
3766
+
"mime",
3767
+
"multer",
3768
+
"nix",
3769
+
"parking_lot",
3770
+
"percent-encoding",
3771
+
"pin-project-lite",
3772
+
"poem-derive",
3773
+
"quick-xml",
3774
+
"regex",
3775
+
"rfc7239",
3776
+
"serde",
3777
+
"serde_json",
3778
+
"serde_urlencoded",
3779
+
"serde_yaml",
3780
+
"smallvec",
3781
+
"sync_wrapper",
3782
+
"tempfile",
3783
+
"thiserror 2.0.12",
3784
+
"tokio",
3785
+
"tokio-stream",
3786
+
"tokio-util",
3787
+
"tracing",
3788
+
"wildmatch",
3789
+
]
3790
+
3791
+
[[package]]
3792
+
name = "poem-derive"
3793
+
version = "3.1.12"
3794
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3795
+
checksum = "056e2fea6de1cb240ffe23cfc4fc370b629f8be83b5f27e16b7acd5231a72de4"
3796
+
dependencies = [
3797
+
"proc-macro-crate",
3798
+
"proc-macro2",
3799
+
"quote",
3800
+
"syn 2.0.103",
3801
+
]
3802
+
3803
+
[[package]]
3804
+
name = "poem-openapi"
3805
+
version = "5.1.16"
3806
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3807
+
checksum = "1ccbcc395bf4dd03df1da32da351b6b6732e4074ce27ddec315650e52a2be44c"
3808
+
dependencies = [
3809
+
"base64 0.22.1",
3810
+
"bytes",
3811
+
"derive_more",
3812
+
"futures-util",
3813
+
"indexmap 2.9.0",
3814
+
"itertools 0.14.0",
3815
+
"mime",
3816
+
"num-traits",
3817
+
"poem",
3818
+
"poem-openapi-derive",
3819
+
"quick-xml",
3820
+
"regex",
3821
+
"serde",
3822
+
"serde_json",
3823
+
"serde_urlencoded",
3824
+
"serde_yaml",
3825
+
"thiserror 2.0.12",
3826
+
"tokio",
3827
+
]
3828
+
3829
+
[[package]]
3830
+
name = "poem-openapi-derive"
3831
+
version = "5.1.16"
3832
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3833
+
checksum = "41273b691a3d467a8c44d05506afba9f7b6bd56c9cdf80123de13fe52d7ec587"
3834
+
dependencies = [
3835
+
"darling 0.20.11",
3836
+
"http",
3837
+
"indexmap 2.9.0",
3838
+
"mime",
3839
+
"proc-macro-crate",
3840
+
"proc-macro2",
3841
+
"quote",
3842
+
"regex",
3843
+
"syn 2.0.103",
3844
+
"thiserror 2.0.12",
3845
+
]
3846
+
3847
+
[[package]]
3732
3848
name = "portable-atomic"
3733
3849
version = "1.11.0"
3734
3850
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3778
3894
]
3779
3895
3780
3896
[[package]]
3897
+
name = "proc-macro-crate"
3898
+
version = "3.3.0"
3899
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3900
+
checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
3901
+
dependencies = [
3902
+
"toml_edit",
3903
+
]
3904
+
3905
+
[[package]]
3781
3906
name = "proc-macro2"
3782
3907
version = "1.0.94"
3783
3908
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3821
3946
"wasi 0.11.0+wasi-snapshot-preview1",
3822
3947
"web-sys",
3823
3948
"winapi",
3949
+
]
3950
+
3951
+
[[package]]
3952
+
name = "quick-xml"
3953
+
version = "0.36.2"
3954
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3955
+
checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
3956
+
dependencies = [
3957
+
"memchr",
3958
+
"serde",
3824
3959
]
3825
3960
3826
3961
[[package]]
···
4086
4221
dependencies = [
4087
4222
"hmac",
4088
4223
"subtle",
4224
+
]
4225
+
4226
+
[[package]]
4227
+
name = "rfc7239"
4228
+
version = "0.1.3"
4229
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4230
+
checksum = "4a82f1d1e38e9a85bb58ffcfadf22ed6f2c94e8cd8581ec2b0f80a2a6858350f"
4231
+
dependencies = [
4232
+
"uncased",
4089
4233
]
4090
4234
4091
4235
[[package]]
···
4554
4698
]
4555
4699
4556
4700
[[package]]
4701
+
name = "serde_yaml"
4702
+
version = "0.9.34+deprecated"
4703
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4704
+
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
4705
+
dependencies = [
4706
+
"indexmap 2.9.0",
4707
+
"itoa",
4708
+
"ryu",
4709
+
"serde",
4710
+
"unsafe-libyaml",
4711
+
]
4712
+
4713
+
[[package]]
4557
4714
name = "sha1"
4558
4715
version = "0.10.6"
4559
4716
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4642
4799
dependencies = [
4643
4800
"clap",
4644
4801
"ctrlc",
4645
-
"dropshot",
4646
4802
"env_logger",
4647
4803
"foyer",
4648
-
"http",
4649
4804
"jetstream",
4650
4805
"log",
4651
4806
"metrics",
4652
4807
"metrics-exporter-prometheus 0.17.2",
4653
-
"schemars",
4654
-
"semver",
4808
+
"poem",
4809
+
"poem-openapi",
4655
4810
"serde",
4656
4811
"serde_json",
4657
4812
"thiserror 2.0.12",
···
5114
5269
]
5115
5270
5116
5271
[[package]]
5272
+
name = "tokio-stream"
5273
+
version = "0.1.17"
5274
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5275
+
checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
5276
+
dependencies = [
5277
+
"futures-core",
5278
+
"pin-project-lite",
5279
+
"tokio",
5280
+
]
5281
+
5282
+
[[package]]
5117
5283
name = "tokio-tungstenite"
5118
5284
version = "0.26.2"
5119
5285
source = "registry+https://github.com/rust-lang/crates.io-index"
···
5422
5588
]
5423
5589
5424
5590
[[package]]
5591
+
name = "uncased"
5592
+
version = "0.9.10"
5593
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5594
+
checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
5595
+
dependencies = [
5596
+
"version_check",
5597
+
]
5598
+
5599
+
[[package]]
5425
5600
name = "unicase"
5426
5601
version = "2.8.1"
5427
5602
source = "registry+https://github.com/rust-lang/crates.io-index"
···
5432
5607
version = "1.0.18"
5433
5608
source = "registry+https://github.com/rust-lang/crates.io-index"
5434
5609
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
5610
+
5611
+
[[package]]
5612
+
name = "unicode-xid"
5613
+
version = "0.2.6"
5614
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5615
+
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
5616
+
5617
+
[[package]]
5618
+
name = "unsafe-libyaml"
5619
+
version = "0.2.11"
5620
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5621
+
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
5435
5622
5436
5623
[[package]]
5437
5624
name = "unsigned-varint"
···
5727
5914
version = "1.2.0"
5728
5915
source = "registry+https://github.com/rust-lang/crates.io-index"
5729
5916
checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
5917
+
5918
+
[[package]]
5919
+
name = "wildmatch"
5920
+
version = "2.4.0"
5921
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5922
+
checksum = "68ce1ab1f8c62655ebe1350f589c61e505cf94d385bc6a12899442d9081e71fd"
5730
5923
5731
5924
[[package]]
5732
5925
name = "winapi"
+2
-4
slingshot/Cargo.toml
+2
-4
slingshot/Cargo.toml
···
6
6
[dependencies]
7
7
clap = { version = "4.5.41", features = ["derive"] }
8
8
ctrlc = "3.4.7"
9
-
dropshot = "0.16.2"
10
9
env_logger = "0.11.8"
11
10
foyer = { version = "0.18.0", features = ["serde"] }
12
-
http = "1.3.1"
13
11
jetstream = { path = "../jetstream", features = ["metrics"] }
14
12
log = "0.4.27"
15
13
metrics = "0.24.2"
16
14
metrics-exporter-prometheus = { version = "0.17.1", features = ["http-listener"] }
17
-
schemars = { version = "0.8.22", features = ["raw_value"] }
18
-
semver = "1.0.26"
15
+
poem = "3.1.12"
16
+
poem-openapi = { version = "5.1.16", features = ["scalar"] }
19
17
serde = { version = "1.0.219", features = ["derive"] }
20
18
serde_json = { version = "1.0.141", features = ["raw_value"] }
21
19
thiserror = "2.0.12"
-10
slingshot/src/error.rs
-10
slingshot/src/error.rs
···
14
14
15
15
#[derive(Debug, Error)]
16
16
pub enum ServerError {
17
-
#[error("failed to configure server logger: {0}")]
18
-
ConfigLogError(std::io::Error),
19
-
#[error("failed to render json for openapi: {0}")]
20
-
OpenApiJsonFail(serde_json::Error),
21
-
#[error(transparent)]
22
-
FailedToBuildServer(#[from] dropshot::BuildError),
23
17
#[error("server exited: {0}")]
24
18
ServerExited(String),
25
-
#[error("server closed badly: {0}")]
26
-
BadClose(String),
27
-
#[error("blahhhahhahha")]
28
-
OhNo(String),
29
19
}
30
20
31
21
#[derive(Debug, Error)]
+153
-268
slingshot/src/server.rs
+153
-268
slingshot/src/server.rs
···
1
-
use serde_json::value::RawValue;
2
-
use crate::CachedRecord;
3
1
use foyer::HybridCache;
4
-
use crate::error::ServerError;
5
-
use dropshot::{
6
-
ApiDescription, Body, ConfigDropshot, ConfigLogging,
7
-
ConfigLoggingLevel, HttpError, HttpResponse, Query, RequestContext,
8
-
ServerBuilder, ServerContext, endpoint,
9
-
ClientErrorStatusCode,
10
-
};
11
-
use http::{
12
-
Response, StatusCode,
13
-
header::{ORIGIN, USER_AGENT},
14
-
};
15
-
use metrics::{counter, histogram};
16
-
use std::sync::Arc;
17
-
18
-
use schemars::JsonSchema;
19
-
use serde::{Deserialize, Serialize};
20
-
use tokio::time::Instant;
2
+
use crate::{error::ServerError, CachedRecord};
21
3
use tokio_util::sync::CancellationToken;
22
4
23
-
const INDEX_HTML: &str = include_str!("../static/index.html");
24
-
const FAVICON: &[u8] = include_bytes!("../static/favicon.ico");
25
-
26
-
pub async fn serve(
27
-
cache: HybridCache<String, CachedRecord>,
28
-
shutdown: CancellationToken,
29
-
) -> Result<(), ServerError> {
30
-
let config_logging = ConfigLogging::StderrTerminal {
31
-
level: ConfigLoggingLevel::Info,
32
-
};
33
-
34
-
let log = config_logging
35
-
.to_logger("example-basic")
36
-
.map_err(ServerError::ConfigLogError)?;
37
-
38
-
let mut api = ApiDescription::new();
39
-
api.register(index).unwrap();
40
-
api.register(favicon).unwrap();
41
-
api.register(openapi).unwrap();
42
-
api.register(get_record).unwrap();
43
-
44
-
// TODO: put spec in a once cell / lazy lock thing?
45
-
let spec = Arc::new(
46
-
api.openapi(
47
-
"Slingshot",
48
-
env!("CARGO_PKG_VERSION")
49
-
.parse()
50
-
.inspect_err(|e| {
51
-
eprintln!("failed to parse cargo package version for openapi: {e:?}")
52
-
})
53
-
.unwrap_or(semver::Version::new(0, 0, 1)),
54
-
)
55
-
.description("A fast edge cache for getRecord")
56
-
.contact_name("part of @microcosm.blue")
57
-
.contact_url("https://microcosm.blue")
58
-
.json()
59
-
.map_err(ServerError::OpenApiJsonFail)?,
60
-
);
5
+
use poem::{listener::TcpListener, Route, Server};
6
+
use poem_openapi::{
7
+
payload::Json,
8
+
param::Query,
9
+
OpenApi, OpenApiService,
10
+
ApiResponse,
11
+
Object,
12
+
types::Example,
13
+
};
61
14
62
-
let sub_shutdown = shutdown.clone();
63
-
let ctx = Context {
64
-
cache,
65
-
spec,
66
-
shutdown: sub_shutdown,
67
-
};
68
-
69
-
let server = ServerBuilder::new(api, ctx, log)
70
-
.config(ConfigDropshot {
71
-
bind_address: "0.0.0.0:9996".parse().unwrap(),
72
-
..Default::default()
73
-
})
74
-
.start()?;
75
-
76
-
tokio::select! {
77
-
s = server.wait_for_shutdown() => {
78
-
s.map_err(ServerError::ServerExited)?;
79
-
log::info!("server shut down normally.");
80
-
},
81
-
_ = shutdown.cancelled() => {
82
-
log::info!("shutting down: closing server");
83
-
server.close().await.map_err(ServerError::BadClose)?;
84
-
},
85
-
}
86
-
Ok(())
15
+
fn example_did() -> String {
16
+
"did:plc:hdhoaan3xa3jiuq4fg4mefid".to_string()
17
+
}
18
+
fn example_collection() -> String {
19
+
"app.bsky.feed.like".to_string()
20
+
}
21
+
fn example_rkey() -> String {
22
+
"3lv4ouczo2b2a".to_string()
87
23
}
88
24
89
-
#[derive(Debug, Clone)]
90
-
struct Context {
91
-
pub cache: HybridCache<String, CachedRecord>,
92
-
pub spec: Arc<serde_json::Value>,
93
-
pub shutdown: CancellationToken,
25
+
#[derive(Object)]
26
+
#[oai(example = true)]
27
+
struct XrpcErrorResponseObject {
28
+
/// Should correspond an error `name` in the lexicon errors array
29
+
error: String,
30
+
/// Human-readable description and possibly additonal context
31
+
message: String,
94
32
}
95
-
96
-
async fn instrument_handler<T, H, R>(ctx: &RequestContext<T>, handler: H) -> Result<R, HttpError>
97
-
where
98
-
R: HttpResponse,
99
-
H: Future<Output = Result<R, HttpError>>,
100
-
T: ServerContext,
101
-
{
102
-
let start = Instant::now();
103
-
let result = handler.await;
104
-
let latency = start.elapsed();
105
-
let status_code = match &result {
106
-
Ok(response) => response.status_code(),
107
-
Err(e) => e.status_code.as_status(),
33
+
impl Example for XrpcErrorResponseObject {
34
+
fn example() -> Self {
35
+
Self {
36
+
error: "RecordNotFound".to_string(),
37
+
message: "This record was deleted".to_string(),
38
+
}
108
39
}
109
-
.as_str() // just the number (.to_string()'s Display does eg `200 OK`)
110
-
.to_string();
111
-
let endpoint = ctx.endpoint.operation_id.clone();
112
-
let headers = ctx.request.headers();
113
-
let origin = headers
114
-
.get(ORIGIN)
115
-
.and_then(|v| v.to_str().ok())
116
-
.unwrap_or("")
117
-
.to_string();
118
-
let ua = headers
119
-
.get(USER_AGENT)
120
-
.and_then(|v| v.to_str().ok())
121
-
.map(|ua| {
122
-
if ua.starts_with("Mozilla/5.0 ") {
123
-
"browser"
124
-
} else {
125
-
ua
126
-
}
127
-
})
128
-
.unwrap_or("")
129
-
.to_string();
130
-
counter!("server_requests_total",
131
-
"endpoint" => endpoint.clone(),
132
-
"origin" => origin,
133
-
"ua" => ua,
134
-
"status_code" => status_code,
135
-
)
136
-
.increment(1);
137
-
histogram!("server_handler_latency", "endpoint" => endpoint).record(latency.as_micros() as f64);
138
-
result
139
40
}
140
41
141
-
use dropshot::{HttpResponseHeaders, HttpResponseOk};
142
42
143
-
pub type OkCorsResponse<T> = Result<HttpResponseHeaders<HttpResponseOk<T>>, HttpError>;
144
-
145
-
/// Helper for constructing Ok responses: return OkCors(T).into()
146
-
/// (not happy with this yet)
147
-
pub struct OkCors<T: Serialize + JsonSchema + Send + Sync>(pub T);
148
-
149
-
impl<T> From<OkCors<T>> for OkCorsResponse<T>
150
-
where
151
-
T: Serialize + JsonSchema + Send + Sync,
152
-
{
153
-
fn from(ok: OkCors<T>) -> OkCorsResponse<T> {
154
-
let mut res = HttpResponseHeaders::new_unnamed(HttpResponseOk(ok.0));
155
-
res.headers_mut()
156
-
.insert("access-control-allow-origin", "*".parse().unwrap());
157
-
Ok(res)
158
-
}
43
+
fn bad_request_handler(err: poem::Error) -> GetRecordResponse {
44
+
GetRecordResponse::BadRequest(Json(XrpcErrorResponseObject {
45
+
error: "InvalidRequest".to_string(),
46
+
message: format!("Bad request, here's some info that maybe should not be exposed: {err}"),
47
+
}))
159
48
}
160
49
161
-
pub fn cors_err(e: HttpError) -> HttpError {
162
-
e.with_header("access-control-allow-origin", "*").unwrap()
50
+
#[derive(Object)]
51
+
#[oai(example = true)]
52
+
struct FoundRecordResponseObject {
53
+
/// at-uri for this record
54
+
uri: String,
55
+
/// CID for this exact version of the record
56
+
///
57
+
/// Slingshot will always return the CID, despite it not being a required
58
+
/// response property in the official lexicon.
59
+
cid: Option<String>,
60
+
/// the record itself as JSON
61
+
value: serde_json::Value,
163
62
}
164
-
165
-
166
-
// TODO: cors for HttpError
167
-
168
-
/// Serve index page as html
169
-
#[endpoint {
170
-
method = GET,
171
-
path = "/",
172
-
/*
173
-
* not useful to have this in openapi
174
-
*/
175
-
unpublished = true,
176
-
}]
177
-
async fn index(ctx: RequestContext<Context>) -> Result<Response<Body>, HttpError> {
178
-
instrument_handler(&ctx, async {
179
-
Ok(Response::builder()
180
-
.status(StatusCode::OK)
181
-
.header(http::header::CONTENT_TYPE, "text/html")
182
-
.body(INDEX_HTML.into())?)
183
-
})
184
-
.await
63
+
impl Example for FoundRecordResponseObject {
64
+
fn example() -> Self {
65
+
Self {
66
+
uri: format!("at://{}/{}/{}", example_did(), example_collection(), example_rkey()),
67
+
cid: Some("bafyreialv3mzvvxaoyrfrwoer3xmabbmdchvrbyhayd7bga47qjbycy74e".to_string()),
68
+
value: serde_json::json!({
69
+
"$type": "app.bsky.feed.like",
70
+
"createdAt": "2025-07-29T18:02:02.327Z",
71
+
"subject": {
72
+
"cid": "bafyreia2gy6eyk5qfetgahvshpq35vtbwy6negpy3gnuulcdi723mi7vxy",
73
+
"uri": "at://did:plc:vwzwgnygau7ed7b7wt5ux7y2/app.bsky.feed.post/3lv4lkb4vgs2k"
74
+
}
75
+
}),
76
+
}
77
+
}
185
78
}
186
79
187
-
/// Serve index page as html
188
-
#[endpoint {
189
-
method = GET,
190
-
path = "/favicon.ico",
191
-
/*
192
-
* not useful to have this in openapi
193
-
*/
194
-
unpublished = true,
195
-
}]
196
-
async fn favicon(ctx: RequestContext<Context>) -> Result<Response<Body>, HttpError> {
197
-
instrument_handler(&ctx, async {
198
-
Ok(Response::builder()
199
-
.status(StatusCode::OK)
200
-
.header(http::header::CONTENT_TYPE, "image/x-icon")
201
-
.body(FAVICON.to_vec().into())?)
202
-
})
203
-
.await
80
+
#[derive(ApiResponse)]
81
+
#[oai(bad_request_handler = "bad_request_handler")]
82
+
enum GetRecordResponse {
83
+
/// Record found
84
+
#[oai(status = 200)]
85
+
Ok(Json<FoundRecordResponseObject>),
86
+
/// Bad request or no record to return
87
+
///
88
+
/// The only error name in the repo.getRecord lexicon is `RecordNotFound`,
89
+
/// but the [canonical api docs](https://docs.bsky.app/docs/api/com-atproto-repo-get-record)
90
+
/// also list `InvalidRequest`, `ExpiredToken`, and `InvalidToken`. Of
91
+
/// these, slingshot will only return `RecordNotFound` or `InvalidRequest`.
92
+
#[oai(status = 400)]
93
+
BadRequest(Json<XrpcErrorResponseObject>),
204
94
}
205
95
206
-
/// Meta: get the openapi spec for this api
207
-
#[endpoint {
208
-
method = GET,
209
-
path = "/openapi",
210
-
/*
211
-
* not useful to have this in openapi
212
-
*/
213
-
unpublished = true,
214
-
}]
215
-
async fn openapi(ctx: RequestContext<Context>) -> OkCorsResponse<serde_json::Value> {
216
-
instrument_handler(&ctx, async {
217
-
let spec = (*ctx.context().spec).clone();
218
-
OkCors(spec).into()
219
-
})
220
-
.await
96
+
struct Xrpc {
97
+
cache: HybridCache<String, CachedRecord>,
221
98
}
222
99
223
-
224
-
#[derive(Debug, Deserialize, JsonSchema)]
225
-
struct GetRecordQuery {
226
-
/// The DID of the repo
227
-
///
228
-
/// NOTE: handles should be accepted here but this is still TODO in slingshot
229
-
pub repo: String,
230
-
/// The NSID of the record collection
231
-
pub collection: String,
232
-
/// The Record key
233
-
pub rkey: String,
234
-
/// Optional: the CID of the version of the record.
100
+
#[OpenApi]
101
+
impl Xrpc {
102
+
/// com.atproto.repo.getRecord
235
103
///
236
-
/// If not specified, then return the most recent version.
104
+
/// Get a single record from a repository. Does not require auth.
237
105
///
238
-
/// If specified and a newer version of the record exists, returns 404 not
239
-
/// found. That is: slingshot only retains the most recent version of a
240
-
/// record.
241
-
#[serde(default)]
242
-
pub cid: Option<String>,
243
-
}
244
-
245
-
#[derive(Debug, Serialize, JsonSchema)]
246
-
struct GetRecordResponse {
247
-
pub uri: String,
248
-
pub cid: String,
249
-
pub value: Box<RawValue>,
250
-
}
251
-
252
-
/// com.atproto.repo.getRecord
253
-
///
254
-
/// Get a single record from a repository. Does not require auth.
255
-
///
256
-
/// See https://docs.bsky.app/docs/api/com-atproto-repo-get-record for the
257
-
/// canonical XRPC documentation that this endpoint aims to be compatible with.
258
-
#[endpoint {
259
-
method = GET,
260
-
path = "/xrpc/com.atproto.repo.getRecord",
261
-
}]
262
-
async fn get_record(
263
-
ctx: RequestContext<Context>,
264
-
query: Query<GetRecordQuery>,
265
-
) -> OkCorsResponse<GetRecordResponse> {
266
-
267
-
let Context { cache, .. } = ctx.context();
268
-
let GetRecordQuery { repo, collection, rkey, cid } = query.into_inner();
106
+
/// See https://docs.bsky.app/docs/api/com-atproto-repo-get-record for the
107
+
/// canonical XRPC documentation that this endpoint aims to be compatible
108
+
/// with.
109
+
#[oai(path = "/com.atproto.repo.getRecord", method = "get")]
110
+
async fn get_record(
111
+
&self,
112
+
/// The DID of the repo
113
+
///
114
+
/// NOTE: handles should be accepted here but this is still TODO in slingshot
115
+
#[oai(example = "example_did")]
116
+
repo: Query<String>,
117
+
/// The NSID of the record collection
118
+
#[oai(example = "example_collection")]
119
+
collection: Query<String>,
120
+
/// The Record key
121
+
#[oai(example = "example_rkey")]
122
+
rkey: Query<String>,
123
+
/// Optional: the CID of the version of the record.
124
+
///
125
+
/// If not specified, then return the most recent version.
126
+
///
127
+
/// If specified and a newer version of the record exists, returns 404 not
128
+
/// found. That is: slingshot only retains the most recent version of a
129
+
/// record.
130
+
cid: Query<Option<String>>,
131
+
) -> GetRecordResponse {
132
+
// TODO: yeah yeah
133
+
let at_uri = format!(
134
+
"at://{}/{}/{}",
135
+
&*repo, &*collection, &*rkey
136
+
);
269
137
270
-
// TODO: yeah yeah
271
-
let at_uri = format!(
272
-
"at://{}/{}/{}",
273
-
&*repo, &*collection, &*rkey
274
-
);
275
-
276
-
instrument_handler(&ctx, async {
277
-
let entry = cache
138
+
let entry = self.cache
278
139
.fetch(at_uri.clone(), || async move {
279
-
Err(foyer::Error::Other(Box::new(ServerError::OhNo("booo".to_string()))))
140
+
todo!()
280
141
})
281
142
.await
282
143
.unwrap();
144
+
145
+
// TODO: actual 404
283
146
284
147
match *entry {
285
148
CachedRecord::Found(ref raw) => {
286
149
let (found_cid, raw_value) = raw.into();
287
150
let found_cid = found_cid.as_ref().to_string();
288
-
if cid.map(|c| c != found_cid).unwrap_or(false) {
289
-
Err(HttpError::for_not_found(None, "CID mismatch".to_string()))
290
-
.map_err(cors_err)?;
151
+
if cid.clone().map(|c| c != found_cid).unwrap_or(false) {
152
+
return GetRecordResponse::BadRequest(Json(XrpcErrorResponseObject {
153
+
error: "RecordNotFound".to_string(),
154
+
message: "A record was found but its CID did not match that requested".to_string(),
155
+
}));
291
156
}
292
-
OkCors(GetRecordResponse {
157
+
// TODO: thank u stellz: https://gist.github.com/stella3d/51e679e55b264adff89d00a1e58d0272
158
+
let value = serde_json::from_str(raw_value.get()).expect("RawValue to be valid json");
159
+
GetRecordResponse::Ok(Json(FoundRecordResponseObject {
293
160
uri: at_uri,
294
-
cid: found_cid,
295
-
value: raw_value,
296
-
}).into()
161
+
cid: Some(found_cid),
162
+
value,
163
+
}))
297
164
},
298
165
CachedRecord::Deleted => {
299
-
Err(HttpError::for_client_error_with_status(
300
-
Some("Gone".to_string()),
301
-
ClientErrorStatusCode::GONE,
302
-
)).map_err(cors_err)
166
+
GetRecordResponse::BadRequest(Json(XrpcErrorResponseObject {
167
+
error: "RecordNotFound".to_string(),
168
+
message: "This record was deleted".to_string(),
169
+
}))
303
170
}
304
171
}
305
-
})
306
-
.await
172
+
}
173
+
}
307
174
175
+
pub async fn serve(
176
+
cache: HybridCache<String, CachedRecord>,
177
+
_shutdown: CancellationToken,
178
+
) -> Result<(), ServerError> {
179
+
let api_service =
180
+
OpenApiService::new(Xrpc { cache }, "Slingshot", env!("CARGO_PKG_VERSION"))
181
+
.server("http://localhost:3000")
182
+
.url_prefix("/xrpc");
183
+
184
+
let app = Route::new()
185
+
.nest("/", api_service.scalar())
186
+
.nest("/openapi.json", api_service.spec_endpoint())
187
+
.nest("/xrpc/", api_service);
188
+
189
+
Server::new(TcpListener::bind("127.0.0.1:3000"))
190
+
.run(app)
191
+
.await
192
+
.map_err(|e| ServerError::ServerExited(format!("uh oh: {e:?}")))
308
193
}