Add subscriptions support for GraphiQL (#619)
* Add subscriptions support on GraphiQL Addresses #501 BREAKING CHANGE: `juniper::http::graphiql::graphiql_source` now requires a second parameter BREAKING CHANGE: `juniper_hyper::graphiql` now requires a second parameter BREAKING CHANGE: `juniper_iron::GraphiQLHandler::new` now requires a second parameter BREAKING CHANGE: `juniper_rocket::graphiql_source` now requires a second parameter BREAKING CHANGE: `juniper_warp::graphiql_filter` now requires a second parameter * Add test where graphiql subscriptions endpoint is not None
This commit is contained in:
parent
f0ccc2e35e
commit
47f7ffaa5b
15 changed files with 117 additions and 19 deletions
juniper
juniper_hyper
juniper_iron
juniper_rocket
juniper_rocket_async/src
juniper_warp
|
@ -18,11 +18,14 @@ See [#419](https://github.com/graphql-rust/juniper/pull/419).
|
||||||
- `SchemaType` is now public
|
- `SchemaType` is now public
|
||||||
- This is helpful when using `context.getSchema()` inside of your field resolvers
|
- This is helpful when using `context.getSchema()` inside of your field resolvers
|
||||||
|
|
||||||
|
- Support subscriptions in GraphiQL
|
||||||
|
|
||||||
See [#569](https://github.com/graphql-rust/juniper/pull/569).
|
See [#569](https://github.com/graphql-rust/juniper/pull/569).
|
||||||
|
|
||||||
## Breaking Changes
|
## Breaking Changes
|
||||||
|
|
||||||
- `juniper::graphiql` has moved to `juniper::http::graphiql`
|
- `juniper::graphiql` has moved to `juniper::http::graphiql`
|
||||||
|
- `juniper::http::graphiql::graphiql_source` now requies a second parameter for subscriptions
|
||||||
|
|
||||||
- remove old `graphql_object!` macro, rename `object` proc macro to `graphql_object`
|
- remove old `graphql_object!` macro, rename `object` proc macro to `graphql_object`
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,23 @@
|
||||||
//! Utility module to generate a GraphiQL interface
|
//! Utility module to generate a GraphiQL interface
|
||||||
|
|
||||||
/// Generate the HTML source to show a GraphiQL interface
|
/// Generate the HTML source to show a GraphiQL interface
|
||||||
pub fn graphiql_source(graphql_endpoint_url: &str) -> String {
|
///
|
||||||
|
/// The subscriptions endpoint URL can optionally be provided. For example:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use juniper::http::graphiql::graphiql_source;
|
||||||
|
/// let graphiql = graphiql_source("/graphql", Some("ws://localhost:8080/subscriptions"));
|
||||||
|
/// ```
|
||||||
|
pub fn graphiql_source(
|
||||||
|
graphql_endpoint_url: &str,
|
||||||
|
subscriptions_endpoint_url: Option<&str>,
|
||||||
|
) -> String {
|
||||||
|
let subscriptions_endpoint = if let Some(sub_url) = subscriptions_endpoint_url {
|
||||||
|
sub_url
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
|
||||||
let stylesheet_source = r#"
|
let stylesheet_source = r#"
|
||||||
<style>
|
<style>
|
||||||
html, body, #app {
|
html, body, #app {
|
||||||
|
@ -14,6 +30,10 @@ pub fn graphiql_source(graphql_endpoint_url: &str) -> String {
|
||||||
"#;
|
"#;
|
||||||
let fetcher_source = r#"
|
let fetcher_source = r#"
|
||||||
<script>
|
<script>
|
||||||
|
if (usingSubscriptions) {
|
||||||
|
var subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient(GRAPHQL_SUBSCRIPTIONS_URL, { reconnect: true });
|
||||||
|
}
|
||||||
|
|
||||||
function graphQLFetcher(params) {
|
function graphQLFetcher(params) {
|
||||||
return fetch(GRAPHQL_URL, {
|
return fetch(GRAPHQL_URL, {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
|
@ -33,9 +53,12 @@ pub fn graphiql_source(graphql_endpoint_url: &str) -> String {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var fetcher = usingSubscriptions ? window.GraphiQLSubscriptionsFetcher.graphQLFetcher(subscriptionsClient, graphQLFetcher) : graphQLFetcher;
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
React.createElement(GraphiQL, {
|
React.createElement(GraphiQL, {
|
||||||
fetcher: graphQLFetcher,
|
fetcher,
|
||||||
}),
|
}),
|
||||||
document.querySelector('#app'));
|
document.querySelector('#app'));
|
||||||
</script>
|
</script>
|
||||||
|
@ -53,16 +76,22 @@ pub fn graphiql_source(graphql_endpoint_url: &str) -> String {
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/fetch/2.0.3/fetch.js"></script>
|
<script src="//cdnjs.cloudflare.com/ajax/libs/fetch/2.0.3/fetch.js"></script>
|
||||||
|
<script src="//unpkg.com/subscriptions-transport-ws@0.8.3/browser/client.js"></script>
|
||||||
|
<script src="//unpkg.com/graphiql-subscriptions-fetcher@0.0.2/browser/client.js"></script>
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
|
<script src="//cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
|
<script src="//cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
|
||||||
<script src="//cdn.jsdelivr.net/npm/graphiql@0.17.5/graphiql.min.js"></script>
|
<script src="//cdn.jsdelivr.net/npm/graphiql@0.17.5/graphiql.min.js"></script>
|
||||||
<script>var GRAPHQL_URL = '{graphql_url}';</script>
|
<script>var GRAPHQL_URL = '{graphql_url}';</script>
|
||||||
|
<script>var usingSubscriptions = {using_subscriptions};</script>
|
||||||
|
<script>var GRAPHQL_SUBSCRIPTIONS_URL = '{graphql_subscriptions_url}';</script>
|
||||||
{fetcher_source}
|
{fetcher_source}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"#,
|
"#,
|
||||||
graphql_url = graphql_endpoint_url,
|
graphql_url = graphql_endpoint_url,
|
||||||
stylesheet_source = stylesheet_source,
|
stylesheet_source = stylesheet_source,
|
||||||
fetcher_source = fetcher_source
|
fetcher_source = fetcher_source,
|
||||||
|
graphql_subscriptions_url = subscriptions_endpoint,
|
||||||
|
using_subscriptions = subscriptions_endpoint_url.is_some(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
- Compatibility with the latest `juniper`.
|
- Compatibility with the latest `juniper`.
|
||||||
|
|
||||||
|
## Breaking Changes
|
||||||
|
|
||||||
|
- `juniper_hyper::graphiql` now requires a second parameter for subscriptions
|
||||||
|
|
||||||
# [[0.5.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_hyper-0.5.2)
|
# [[0.5.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_hyper-0.5.2)
|
||||||
|
|
||||||
- Compatibility with the latest `juniper`.
|
- Compatibility with the latest `juniper`.
|
||||||
|
|
|
@ -31,7 +31,7 @@ async fn main() {
|
||||||
let ctx = ctx.clone();
|
let ctx = ctx.clone();
|
||||||
async move {
|
async move {
|
||||||
match (req.method(), req.uri().path()) {
|
match (req.method(), req.uri().path()) {
|
||||||
(&Method::GET, "/") => juniper_hyper::graphiql("/graphql").await,
|
(&Method::GET, "/") => juniper_hyper::graphiql("/graphql", None).await,
|
||||||
(&Method::GET, "/graphql") | (&Method::POST, "/graphql") => {
|
(&Method::GET, "/graphql") | (&Method::POST, "/graphql") => {
|
||||||
juniper_hyper::graphql(root_node, ctx, req).await
|
juniper_hyper::graphql(root_node, ctx, req).await
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,10 +114,16 @@ async fn parse_post_req<S: ScalarValue>(
|
||||||
.map_err(GraphQLRequestError::BodyJSONError)
|
.map_err(GraphQLRequestError::BodyJSONError)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn graphiql(graphql_endpoint: &str) -> Result<Response<Body>, hyper::Error> {
|
pub async fn graphiql(
|
||||||
|
graphql_endpoint: &str,
|
||||||
|
subscriptions_endpoint: Option<&str>,
|
||||||
|
) -> Result<Response<Body>, hyper::Error> {
|
||||||
let mut resp = new_html_response(StatusCode::OK);
|
let mut resp = new_html_response(StatusCode::OK);
|
||||||
// XXX: is the call to graphiql_source blocking?
|
// XXX: is the call to graphiql_source blocking?
|
||||||
*resp.body_mut() = Body::from(juniper::http::graphiql::graphiql_source(graphql_endpoint));
|
*resp.body_mut() = Body::from(juniper::http::graphiql::graphiql_source(
|
||||||
|
graphql_endpoint,
|
||||||
|
subscriptions_endpoint,
|
||||||
|
));
|
||||||
Ok(resp)
|
Ok(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
- Compatibility with the latest `juniper`.
|
- Compatibility with the latest `juniper`.
|
||||||
|
|
||||||
|
## Breaking Changes
|
||||||
|
|
||||||
|
- `juniper_iron::GraphiQLHandler::new` now requires a second parameter for subscriptions
|
||||||
|
|
||||||
# [[0.6.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_iron-0.6.2)
|
# [[0.6.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_iron-0.6.2)
|
||||||
|
|
||||||
- Compatibility with the latest `juniper`.
|
- Compatibility with the latest `juniper`.
|
||||||
|
|
|
@ -29,7 +29,7 @@ fn main() {
|
||||||
EmptyMutation::<Database>::new(),
|
EmptyMutation::<Database>::new(),
|
||||||
EmptySubscription::<Database>::new(),
|
EmptySubscription::<Database>::new(),
|
||||||
);
|
);
|
||||||
let graphiql_endpoint = GraphiQLHandler::new("/graphql");
|
let graphiql_endpoint = GraphiQLHandler::new("/graphql", None);
|
||||||
|
|
||||||
mount.mount("/", graphiql_endpoint);
|
mount.mount("/", graphiql_endpoint);
|
||||||
mount.mount("/graphql", graphql_endpoint);
|
mount.mount("/graphql", graphql_endpoint);
|
||||||
|
|
|
@ -159,6 +159,7 @@ pub struct GraphQLHandler<
|
||||||
/// Handler that renders `GraphiQL` - a graphical query editor interface
|
/// Handler that renders `GraphiQL` - a graphical query editor interface
|
||||||
pub struct GraphiQLHandler {
|
pub struct GraphiQLHandler {
|
||||||
graphql_url: String,
|
graphql_url: String,
|
||||||
|
subscription_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handler that renders `GraphQL Playground` - a graphical query editor interface
|
/// Handler that renders `GraphQL Playground` - a graphical query editor interface
|
||||||
|
@ -275,9 +276,10 @@ impl GraphiQLHandler {
|
||||||
///
|
///
|
||||||
/// The provided URL should point to the URL of the attached `GraphQLHandler`. It can be
|
/// The provided URL should point to the URL of the attached `GraphQLHandler`. It can be
|
||||||
/// relative, so a common value could be `"/graphql"`.
|
/// relative, so a common value could be `"/graphql"`.
|
||||||
pub fn new(graphql_url: &str) -> GraphiQLHandler {
|
pub fn new(graphql_url: &str, subscription_url: Option<&str>) -> GraphiQLHandler {
|
||||||
GraphiQLHandler {
|
GraphiQLHandler {
|
||||||
graphql_url: graphql_url.to_owned(),
|
graphql_url: graphql_url.to_owned(),
|
||||||
|
subscription_url: subscription_url.map(|s| s.to_owned()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -326,7 +328,10 @@ impl Handler for GraphiQLHandler {
|
||||||
Ok(Response::with((
|
Ok(Response::with((
|
||||||
content_type,
|
content_type,
|
||||||
status::Ok,
|
status::Ok,
|
||||||
juniper::http::graphiql::graphiql_source(&self.graphql_url),
|
juniper::http::graphiql::graphiql_source(
|
||||||
|
&self.graphql_url,
|
||||||
|
self.subscription_url.as_deref(),
|
||||||
|
),
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
- Compatibility with the latest `juniper`.
|
- Compatibility with the latest `juniper`.
|
||||||
- Rocket integration does not require default features.
|
- Rocket integration does not require default features.
|
||||||
|
|
||||||
|
## Breaking Changes
|
||||||
|
|
||||||
|
- `juniper_rocket::graphiql_source` now requires a second parameter for subscriptions
|
||||||
|
|
||||||
# [[0.5.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_rocket-0.5.2)
|
# [[0.5.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_rocket-0.5.2)
|
||||||
|
|
||||||
- Compatibility with the latest `juniper`.
|
- Compatibility with the latest `juniper`.
|
||||||
|
|
|
@ -11,7 +11,7 @@ type Schema = RootNode<'static, Query, EmptyMutation<Database>, EmptySubscriptio
|
||||||
|
|
||||||
#[rocket::get("/")]
|
#[rocket::get("/")]
|
||||||
fn graphiql() -> content::Html<String> {
|
fn graphiql() -> content::Html<String> {
|
||||||
juniper_rocket::graphiql_source("/graphql")
|
juniper_rocket::graphiql_source("/graphql", None)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rocket::get("/graphql?<request>")]
|
#[rocket::get("/graphql?<request>")]
|
||||||
|
|
|
@ -71,9 +71,13 @@ where
|
||||||
pub struct GraphQLResponse(pub Status, pub String);
|
pub struct GraphQLResponse(pub Status, pub String);
|
||||||
|
|
||||||
/// Generate an HTML page containing GraphiQL
|
/// Generate an HTML page containing GraphiQL
|
||||||
pub fn graphiql_source(graphql_endpoint_url: &str) -> content::Html<String> {
|
pub fn graphiql_source(
|
||||||
|
graphql_endpoint_url: &str,
|
||||||
|
subscriptions_endpoint: Option<&str>,
|
||||||
|
) -> content::Html<String> {
|
||||||
content::Html(juniper::http::graphiql::graphiql_source(
|
content::Html(juniper::http::graphiql::graphiql_source(
|
||||||
graphql_endpoint_url,
|
graphql_endpoint_url,
|
||||||
|
subscriptions_endpoint,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -74,6 +74,7 @@ pub struct GraphQLResponse(pub Status, pub String);
|
||||||
pub fn graphiql_source(graphql_endpoint_url: &str) -> content::Html<String> {
|
pub fn graphiql_source(graphql_endpoint_url: &str) -> content::Html<String> {
|
||||||
content::Html(juniper::http::graphiql::graphiql_source(
|
content::Html(juniper::http::graphiql::graphiql_source(
|
||||||
graphql_endpoint_url,
|
graphql_endpoint_url,
|
||||||
|
None,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ to `juniper` to be reused in other http integrations, since this implementation
|
||||||
- Update `playground_filter` to support subscription endpoint URLs
|
- Update `playground_filter` to support subscription endpoint URLs
|
||||||
- Update `warp` to 0.2
|
- Update `warp` to 0.2
|
||||||
- Rename synchronous `execute` to `execute_sync`, add asynchronous `execute`
|
- Rename synchronous `execute` to `execute_sync`, add asynchronous `execute`
|
||||||
|
- `juniper_warp::graphiql_filter` now requires a second parameter for subscriptions
|
||||||
|
|
||||||
# [[0.5.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_warp-0.5.2)
|
# [[0.5.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_warp-0.5.2)
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ async fn main() {
|
||||||
warp::serve(
|
warp::serve(
|
||||||
warp::get()
|
warp::get()
|
||||||
.and(warp::path("graphiql"))
|
.and(warp::path("graphiql"))
|
||||||
.and(juniper_warp::graphiql_filter("/graphql"))
|
.and(juniper_warp::graphiql_filter("/graphql", None))
|
||||||
.or(homepage)
|
.or(homepage)
|
||||||
.or(warp::path("graphql").and(graphql_filter))
|
.or(warp::path("graphql").and(graphql_filter))
|
||||||
.with(log),
|
.with(log),
|
||||||
|
|
|
@ -299,20 +299,41 @@ type Response = Pin<
|
||||||
/// # use warp::Filter;
|
/// # use warp::Filter;
|
||||||
/// # use juniper_warp::graphiql_filter;
|
/// # use juniper_warp::graphiql_filter;
|
||||||
/// #
|
/// #
|
||||||
/// let graphiql_route = warp::path("graphiql").and(graphiql_filter("/graphql"));
|
/// let graphiql_route = warp::path("graphiql").and(graphiql_filter("/graphql",
|
||||||
|
/// None));
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Or with subscriptions support, provide the subscriptions endpoint URL:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # extern crate warp;
|
||||||
|
/// # extern crate juniper_warp;
|
||||||
|
/// #
|
||||||
|
/// # use warp::Filter;
|
||||||
|
/// # use juniper_warp::graphiql_filter;
|
||||||
|
/// #
|
||||||
|
/// let graphiql_route = warp::path("graphiql").and(graphiql_filter("/graphql",
|
||||||
|
/// Some("ws://localhost:8080/subscriptions")));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn graphiql_filter(
|
pub fn graphiql_filter(
|
||||||
graphql_endpoint_url: &'static str,
|
graphql_endpoint_url: &'static str,
|
||||||
|
subscriptions_endpoint: Option<&'static str>,
|
||||||
) -> warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)> {
|
) -> warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)> {
|
||||||
warp::any()
|
warp::any()
|
||||||
.map(move || graphiql_response(graphql_endpoint_url))
|
.map(move || graphiql_response(graphql_endpoint_url, subscriptions_endpoint))
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn graphiql_response(graphql_endpoint_url: &'static str) -> warp::http::Response<Vec<u8>> {
|
fn graphiql_response(
|
||||||
|
graphql_endpoint_url: &'static str,
|
||||||
|
subscriptions_endpoint: Option<&'static str>,
|
||||||
|
) -> warp::http::Response<Vec<u8>> {
|
||||||
warp::http::Response::builder()
|
warp::http::Response::builder()
|
||||||
.header("content-type", "text/html;charset=utf-8")
|
.header("content-type", "text/html;charset=utf-8")
|
||||||
.body(juniper::http::graphiql::graphiql_source(graphql_endpoint_url).into_bytes())
|
.body(
|
||||||
|
juniper::http::graphiql::graphiql_source(graphql_endpoint_url, subscriptions_endpoint)
|
||||||
|
.into_bytes(),
|
||||||
|
)
|
||||||
.expect("response is valid")
|
.expect("response is valid")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -568,14 +589,14 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn graphiql_response_does_not_panic() {
|
fn graphiql_response_does_not_panic() {
|
||||||
graphiql_response("/abcd");
|
graphiql_response("/abcd", None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn graphiql_endpoint_matches() {
|
async fn graphiql_endpoint_matches() {
|
||||||
let filter = warp::get()
|
let filter = warp::get()
|
||||||
.and(warp::path("graphiql"))
|
.and(warp::path("graphiql"))
|
||||||
.and(graphiql_filter("/graphql"));
|
.and(graphiql_filter("/graphql", None));
|
||||||
let result = request()
|
let result = request()
|
||||||
.method("GET")
|
.method("GET")
|
||||||
.path("/graphiql")
|
.path("/graphiql")
|
||||||
|
@ -591,7 +612,7 @@ mod tests {
|
||||||
let filter = warp::get()
|
let filter = warp::get()
|
||||||
.and(warp::path("dogs-api"))
|
.and(warp::path("dogs-api"))
|
||||||
.and(warp::path("graphiql"))
|
.and(warp::path("graphiql"))
|
||||||
.and(graphiql_filter("/dogs-api/graphql"));
|
.and(graphiql_filter("/dogs-api/graphql", None));
|
||||||
let response = request()
|
let response = request()
|
||||||
.method("GET")
|
.method("GET")
|
||||||
.path("/dogs-api/graphiql")
|
.path("/dogs-api/graphiql")
|
||||||
|
@ -609,6 +630,22 @@ mod tests {
|
||||||
assert!(body.contains("<script>var GRAPHQL_URL = '/dogs-api/graphql';</script>"));
|
assert!(body.contains("<script>var GRAPHQL_URL = '/dogs-api/graphql';</script>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn graphiql_endpoint_with_subscription_matches() {
|
||||||
|
let filter = warp::get().and(warp::path("graphiql")).and(graphiql_filter(
|
||||||
|
"/graphql",
|
||||||
|
Some("ws:://localhost:8080/subscriptions"),
|
||||||
|
));
|
||||||
|
let result = request()
|
||||||
|
.method("GET")
|
||||||
|
.path("/graphiql")
|
||||||
|
.header("accept", "text/html")
|
||||||
|
.filter(&filter)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn playground_endpoint_matches() {
|
async fn playground_endpoint_matches() {
|
||||||
let filter = warp::get()
|
let filter = warp::get()
|
||||||
|
|
Loading…
Add table
Reference in a new issue