Skip to content

Commit 244427e

Browse files
committed
feat: endpoint /api/v1/genes/search with OpenAPI (#575)
1 parent a474dd0 commit 244427e

File tree

3 files changed

+227
-41
lines changed

3 files changed

+227
-41
lines changed

openapi.schema.yaml

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,50 @@ paths:
3939
application/json:
4040
schema:
4141
$ref: '#/components/schemas/CustomError'
42+
/api/v1/genes/search:
43+
get:
44+
tags:
45+
- genes_search
46+
summary: Search for genes.
47+
operationId: genesSearch
48+
parameters:
49+
- name: q
50+
in: query
51+
description: The string to search for.
52+
required: true
53+
schema:
54+
type: string
55+
- name: fields
56+
in: query
57+
description: The fields to search in.
58+
required: false
59+
schema:
60+
type:
61+
- array
62+
- 'null'
63+
items:
64+
$ref: '#/components/schemas/GenesFields'
65+
- name: case_sensitive
66+
in: query
67+
description: Enable case sensitive search.
68+
required: false
69+
schema:
70+
type:
71+
- boolean
72+
- 'null'
73+
responses:
74+
'200':
75+
description: Genes search results.
76+
content:
77+
application/json:
78+
schema:
79+
$ref: '#/components/schemas/GenesSearchResponse'
80+
'500':
81+
description: Internal server error.
82+
content:
83+
application/json:
84+
schema:
85+
$ref: '#/components/schemas/CustomError'
4286
/api/v1/versionsInfo:
4387
get:
4488
tags:
@@ -84,6 +128,45 @@ components:
84128
properties:
85129
err:
86130
type: string
131+
GeneNames:
132+
type: object
133+
description: Identifier / name information for one gene.
134+
required:
135+
- hgnc_id
136+
- symbol
137+
- name
138+
- alias_symbol
139+
- alias_name
140+
properties:
141+
hgnc_id:
142+
type: string
143+
description: HGNC gene ID.
144+
symbol:
145+
type: string
146+
description: HGNC gene symbol.
147+
name:
148+
type: string
149+
description: Gene name from HGNC.
150+
alias_symbol:
151+
type: array
152+
items:
153+
type: string
154+
description: HGNC alias symbols.
155+
alias_name:
156+
type: array
157+
items:
158+
type: string
159+
description: HGNC alias names.
160+
ensembl_gene_id:
161+
type:
162+
- string
163+
- 'null'
164+
description: ENSEMBL gene ID.
165+
ncbi_gene_id:
166+
type:
167+
- string
168+
- 'null'
169+
description: NCBI gene ID.
87170
GenesAcmgSecondaryFindingRecord:
88171
type: object
89172
description: Information from ACMG secondary findings list.
@@ -982,6 +1065,17 @@ components:
9821065
- Gene
9831066
- Str
9841067
- Region
1068+
GenesFields:
1069+
type: string
1070+
description: The allowed fields to search in.
1071+
enum:
1072+
- hgnc_id
1073+
- symbol
1074+
- name
1075+
- alias_symbol
1076+
- alias_name
1077+
- ensembl_gene_id
1078+
- ncbi_gene_id
9851079
GenesGeneData:
9861080
type: object
9871081
description: Gene identity information.
@@ -2131,6 +2225,38 @@ components:
21312225
format: int32
21322226
minimum: 0
21332227
description: PubMed IDs.
2228+
GenesSearchQuery:
2229+
type: object
2230+
description: Parameters for `handle`.
2231+
required:
2232+
- q
2233+
properties:
2234+
q:
2235+
type: string
2236+
description: The string to search for.
2237+
fields:
2238+
type:
2239+
- array
2240+
- 'null'
2241+
items:
2242+
$ref: '#/components/schemas/GenesFields'
2243+
description: The fields to search in.
2244+
case_sensitive:
2245+
type:
2246+
- boolean
2247+
- 'null'
2248+
description: Enable case sensitive search.
2249+
GenesSearchResponse:
2250+
type: object
2251+
description: Result for `handle`.
2252+
required:
2253+
- genes
2254+
properties:
2255+
genes:
2256+
type: array
2257+
items:
2258+
$ref: '#/components/schemas/Scored'
2259+
description: The resulting gene information.
21342260
GenesShetRecord:
21352261
type: object
21362262
description: Entry with sHet information (Weghorn et al., 2019).
@@ -2151,6 +2277,20 @@ components:
21512277
enum:
21522278
- grch37
21532279
- grch38
2280+
Scored:
2281+
type: object
2282+
description: A scored result.
2283+
required:
2284+
- score
2285+
- data
2286+
properties:
2287+
score:
2288+
type: number
2289+
format: float
2290+
description: The score.
2291+
data:
2292+
$ref: '#/components/schemas/GeneNames'
2293+
description: The result.
21542294
VersionsAnnotationInfo:
21552295
type: object
21562296
description: Version information for one database.

src/server/run/genes_search.rs

Lines changed: 71 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
//! Implementation of `/genes/search` that allows to search for genes by symbol etc.
1+
//! Implementation of endpoint `/api/v1/genes/search`.
2+
//!
3+
//! Also includes the implementation of the `/genes/search` endpoint (deprecated).
24
//!
35
//! Gene identifiers (HGNC, NCBI, ENSEMBL) must match. As for symbols and names, the
46
//! search string may also be a substring.
57
use actix_web::{
68
get,
79
web::{self, Data, Json, Path},
8-
Responder,
910
};
1011

1112
use crate::server::run::GeneNames;
@@ -15,19 +16,19 @@ use serde_with::{formats::CommaSeparator, StringWithSeparator};
1516

1617
/// The allowed fields to search in.
1718
#[derive(
18-
serde::Serialize,
19-
serde::Deserialize,
20-
strum::Display,
21-
strum::EnumString,
2219
Debug,
2320
Clone,
2421
Copy,
2522
PartialEq,
2623
Eq,
24+
strum::Display,
25+
strum::EnumString,
26+
serde::Serialize,
27+
serde::Deserialize,
28+
utoipa::ToSchema,
2729
)]
2830
#[serde(rename_all = "snake_case")]
29-
#[strum(serialize_all = "snake_case")]
30-
enum Fields {
31+
pub(crate) enum GenesFields {
3132
/// HGNC ID field
3233
HgncId,
3334
/// Symbol field
@@ -47,46 +48,48 @@ enum Fields {
4748
/// Parameters for `handle`.
4849
#[serde_with::skip_serializing_none]
4950
#[serde_with::serde_as]
50-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
51+
#[derive(
52+
Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema, utoipa::IntoParams,
53+
)]
5154
#[serde(rename_all = "snake_case")]
52-
struct Request {
55+
pub(crate) struct GenesSearchQuery {
5356
/// The string to search for.
5457
pub q: String,
5558
/// The fields to search in.
56-
#[serde_as(as = "Option<StringWithSeparator::<CommaSeparator, Fields>>")]
57-
pub fields: Option<Vec<Fields>>,
59+
#[serde_as(as = "Option<StringWithSeparator::<CommaSeparator, GenesFields>>")]
60+
pub fields: Option<Vec<GenesFields>>,
5861
/// Enable case sensitive search.
5962
pub case_sensitive: Option<bool>,
6063
}
6164

6265
/// A scored result.
63-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
64-
struct Scored<T> {
66+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema)]
67+
pub(crate) struct Scored<T> {
6568
/// The score.
6669
pub score: f32,
6770
/// The result.
6871
pub data: T,
6972
}
7073

74+
/// Alias for scored genes names.
75+
pub(crate) type GenesScoredGeneNames = Scored<GeneNames>;
76+
7177
/// Result for `handle`.
72-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
78+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema)]
7379
#[serde_with::skip_serializing_none]
74-
struct Container {
75-
// TODO: add data version
80+
pub(crate) struct GenesSearchResponse {
7681
/// The resulting gene information.
77-
pub genes: Vec<Scored<GeneNames>>,
82+
pub genes: Vec<GenesScoredGeneNames>,
7883
}
7984

80-
/// Query for annotations for one variant.
81-
#[allow(clippy::option_map_unit_fn)]
82-
#[get("/genes/search")]
83-
async fn handle(
85+
/// Implementation of both endpoints.
86+
async fn handle_impl(
8487
data: Data<crate::server::run::WebServerData>,
8588
_path: Path<()>,
86-
query: web::Query<Request>,
87-
) -> actix_web::Result<impl Responder, CustomError> {
89+
query: web::Query<GenesSearchQuery>,
90+
) -> actix_web::Result<Json<GenesSearchResponse>, CustomError> {
8891
if query.q.len() < 2 {
89-
return Ok(Json(Container {
92+
return Ok(Json(GenesSearchResponse {
9093
// server_version: VERSION.to_string(),
9194
// builder_version,
9295
genes: Vec::new(),
@@ -120,35 +123,36 @@ async fn handle(
120123
val.to_lowercase().contains(&q)
121124
}
122125
};
123-
let fields: Vec<Fields> = if let Some(fields) = query.fields.as_ref() {
126+
let fields: Vec<GenesFields> = if let Some(fields) = query.fields.as_ref() {
124127
fields.clone()
125128
} else {
126129
Vec::new()
127130
};
128131

129132
// The fields contain the given field or are empty.
130-
let fields_contains = |field: &Fields| -> bool { fields.is_empty() || fields.contains(field) };
133+
let fields_contains =
134+
|field: &GenesFields| -> bool { fields.is_empty() || fields.contains(field) };
131135

132136
let mut genes = genes_db
133137
.data
134138
.gene_names
135139
.iter()
136140
.map(|gn| -> Scored<GeneNames> {
137-
let score = if (fields_contains(&Fields::HgncId) && equals_q(&gn.hgnc_id))
138-
|| (fields_contains(&Fields::Symbol) && equals_q(&gn.symbol))
139-
|| (fields_contains(&Fields::Symbol) && equals_q(&gn.symbol))
140-
|| (fields_contains(&Fields::Name) && equals_q(&gn.name))
141-
|| (fields_contains(&Fields::EnsemblGeneId)
141+
let score = if (fields_contains(&GenesFields::HgncId) && equals_q(&gn.hgnc_id))
142+
|| (fields_contains(&GenesFields::Symbol) && equals_q(&gn.symbol))
143+
|| (fields_contains(&GenesFields::Symbol) && equals_q(&gn.symbol))
144+
|| (fields_contains(&GenesFields::Name) && equals_q(&gn.name))
145+
|| (fields_contains(&GenesFields::EnsemblGeneId)
142146
&& gn.ensembl_gene_id.iter().any(|s| equals_q(s)))
143-
|| (fields_contains(&Fields::NcbiGeneId)
147+
|| (fields_contains(&GenesFields::NcbiGeneId)
144148
&& gn.ncbi_gene_id.iter().any(|s| equals_q(s)))
145149
{
146150
1f32
147-
} else if fields_contains(&Fields::Symbol) && contains_q(&gn.symbol) {
151+
} else if fields_contains(&GenesFields::Symbol) && contains_q(&gn.symbol) {
148152
q.len() as f32 / gn.symbol.len() as f32
149-
} else if fields_contains(&Fields::Name) && contains_q(&gn.name) {
153+
} else if fields_contains(&GenesFields::Name) && contains_q(&gn.name) {
150154
q.len() as f32 / gn.name.len() as f32
151-
} else if fields_contains(&Fields::AliasSymbol)
155+
} else if fields_contains(&GenesFields::AliasSymbol)
152156
&& gn.alias_symbol.iter().any(|s| contains_q(s))
153157
{
154158
gn.alias_symbol
@@ -162,7 +166,7 @@ async fn handle(
162166
})
163167
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
164168
.unwrap_or(0f32)
165-
} else if fields_contains(&Fields::AliasName)
169+
} else if fields_contains(&GenesFields::AliasName)
166170
&& gn.alias_name.iter().any(|s| contains_q(s))
167171
{
168172
gn.alias_name
@@ -194,9 +198,38 @@ async fn handle(
194198
.unwrap_or(std::cmp::Ordering::Equal)
195199
});
196200

197-
Ok(Json(Container {
201+
Ok(Json(GenesSearchResponse {
198202
// server_version: VERSION.to_string(),
199203
// builder_version,
200204
genes,
201205
}))
202206
}
207+
208+
/// Search for genes.
209+
#[get("/genes/search")]
210+
async fn handle(
211+
data: Data<crate::server::run::WebServerData>,
212+
path: Path<()>,
213+
query: web::Query<GenesSearchQuery>,
214+
) -> actix_web::Result<Json<GenesSearchResponse>, CustomError> {
215+
handle_impl(data, path, query).await
216+
}
217+
218+
/// Search for genes.
219+
#[utoipa::path(
220+
get,
221+
operation_id = "genesSearch",
222+
params(GenesSearchQuery),
223+
responses(
224+
(status = 200, description = "Genes search results.", body = GenesSearchResponse),
225+
(status = 500, description = "Internal server error.", body = CustomError)
226+
)
227+
)]
228+
#[get("/api/v1/genes/search")]
229+
async fn handle_with_openapi(
230+
data: Data<crate::server::run::WebServerData>,
231+
path: Path<()>,
232+
query: web::Query<GenesSearchQuery>,
233+
) -> actix_web::Result<Json<GenesSearchResponse>, CustomError> {
234+
handle_impl(data, path, query).await
235+
}

0 commit comments

Comments
 (0)