Skip to content

Commit e83165f

Browse files
Merge #1068
1068: Begin refactoring module layout r=carols10cents This PR begins some work towards refactoring the module layout of our rust code. This PR includes several initial changes: * Group route definitions in `lib.rs` and document where those api endpoints are currently used. * Move the `krate` and `version` api modules into subdirectories. This will be explained further below. * Move `categories.rs` and `categories.toml` under a new `boot` module as these are only used during boot of the server process. * Pull github related functionality out of `http` into a separate `github` module, leaving the http security middleware behind. The second bullet point sets the groundwork for changes similar to what I prototyped in [this branch](3f67a0e...jtgeibel:refactor-api-into-submodules). I'm hoping that by moving and renaming these to `mod.rs` now, without actually changing the files yet, will cut down on merge conflicts with any future PRs. # Proposed further changes As proposed in the previous branch, I believe the next step would be to split the `krate` and `version` modules into submodules which support `cargo` and `frontend` functionality. At the moment, the `api/v1/crates` route is the only one used by both. My main motivation for this is that for `cargo` routes we need to strictly maintain backwards compatibility. For instance, as noted in #712, we always return a `200` response code with an `errors` array because this is expected by `cargo` (with the exception of `krate::download` where it does not). If we ever decide to change this then we will need to take care that we don't impact routes used by cargo. (Of course we need to be careful in general, as we've broken unknown external users in the past, but hopefully this will provide an extra speed bump when authoring and reviewing changes.) While experimenting on that branch I also got a better feel for how we could split logic between the api/controller level and the model/view level. My notes on this may be a bit dated, but in some modules we mix api functionality with diesel and serde structs. For instance, the following files contain logic for all these concerns: * `keyword.rs` * `krate/mod.rs` * `token.rs` * `user/mod.rs` * `version/mod.rs` In contrast, the following files contain only diesel and serde functionality, in support of api calls handled by the files above: * `category.rs` * `dependency.rs` * `download.rs` * `owner.rs` It would be nice if we could pull out the database and json related functionality in the first list, and then group all of those files in a new submodule. At this time, I do not think it is necessary to try to split apart the model and view functionality as this logic is very straightforward. For now, I hope this is a small enough PR to review independently and provide a place to discuss these suggestions for further refactoring. /cc @vignesh-sankaran
2 parents 1fb0049 + 7e88ba5 commit e83165f

File tree

13 files changed

+139
-126
lines changed

13 files changed

+139
-126
lines changed

src/bin/server.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ fn main() {
5050

5151
// On every server restart, ensure the categories available in the database match
5252
// the information in *src/categories.toml*.
53-
let categories_toml = include_str!("../categories.toml");
54-
cargo_registry::categories::sync(&categories_toml).unwrap();
53+
let categories_toml = include_str!("../boot/categories.toml");
54+
cargo_registry::boot::categories::sync(&categories_toml).unwrap();
5555

5656
let heroku = env::var("HEROKU").is_ok();
5757
let port = if heroku {
File renamed without changes.
File renamed without changes.

src/boot/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod categories;

src/github.rs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
//! This module implements functionality for interacting with GitHub.
2+
3+
use curl;
4+
use curl::easy::{Easy, List};
5+
6+
use oauth2::*;
7+
8+
use serde_json;
9+
use serde::Deserialize;
10+
11+
use std::str;
12+
13+
use app::App;
14+
use util::{human, internal, CargoResult, ChainError};
15+
16+
/// Does all the nonsense for sending a GET to Github. Doesn't handle parsing
17+
/// because custom error-code handling may be desirable. Use
18+
/// `parse_github_response` to handle the "common" processing of responses.
19+
pub fn github(app: &App, url: &str, auth: &Token) -> Result<(Easy, Vec<u8>), curl::Error> {
20+
let url = format!("{}://api.github.com{}", app.config.api_protocol, url);
21+
info!("GITHUB HTTP: {}", url);
22+
23+
let mut headers = List::new();
24+
headers
25+
.append("Accept: application/vnd.github.v3+json")
26+
.unwrap();
27+
headers.append("User-Agent: hello!").unwrap();
28+
headers
29+
.append(&format!("Authorization: token {}", auth.access_token))
30+
.unwrap();
31+
32+
let mut handle = app.handle();
33+
handle.url(&url).unwrap();
34+
handle.get(true).unwrap();
35+
handle.http_headers(headers).unwrap();
36+
37+
let mut data = Vec::new();
38+
{
39+
let mut transfer = handle.transfer();
40+
transfer
41+
.write_function(|buf| {
42+
data.extend_from_slice(buf);
43+
Ok(buf.len())
44+
})
45+
.unwrap();
46+
transfer.perform()?;
47+
}
48+
Ok((handle, data))
49+
}
50+
51+
/// Checks for normal responses
52+
pub fn parse_github_response<'de, 'a: 'de, T: Deserialize<'de>>(
53+
mut resp: Easy,
54+
data: &'a [u8],
55+
) -> CargoResult<T> {
56+
match resp.response_code().unwrap() {
57+
200 => {}
58+
// Ok!
59+
403 => {
60+
return Err(human(
61+
"It looks like you don't have permission \
62+
to query a necessary property from Github \
63+
to complete this request. \
64+
You may need to re-authenticate on \
65+
crates.io to grant permission to read \
66+
github org memberships. Just go to \
67+
https://crates.io/login",
68+
));
69+
}
70+
n => {
71+
let resp = String::from_utf8_lossy(data);
72+
return Err(internal(&format_args!(
73+
"didn't get a 200 result from \
74+
github, got {} with: {}",
75+
n,
76+
resp
77+
)));
78+
}
79+
}
80+
81+
let json = str::from_utf8(data)
82+
.ok()
83+
.chain_error(|| internal("github didn't send a utf8-response"))?;
84+
85+
serde_json::from_str(json).chain_error(|| internal("github didn't send a valid json response"))
86+
}
87+
88+
/// Gets a token with the given string as the access token, but all
89+
/// other info null'd out. Generally, just to be fed to the `github` fn.
90+
pub fn token(token: String) -> Token {
91+
Token {
92+
access_token: token,
93+
scopes: Vec::new(),
94+
token_type: String::new(),
95+
}
96+
}

src/http.rs

Lines changed: 3 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,14 @@
1+
//! This module implements middleware for adding secuirty headers to
2+
//! http responses in production.
3+
14
use conduit::{Request, Response};
25
use conduit_middleware::Middleware;
36

4-
use curl;
5-
use curl::easy::{Easy, List};
6-
7-
use oauth2::*;
8-
9-
use serde_json;
10-
use serde::Deserialize;
11-
12-
use std::str;
137
use std::error::Error;
148
use std::collections::HashMap;
159

16-
use app::App;
17-
use util::{human, internal, CargoResult, ChainError};
1810
use Uploader;
1911

20-
/// Does all the nonsense for sending a GET to Github. Doesn't handle parsing
21-
/// because custom error-code handling may be desirable. Use
22-
/// `parse_github_response` to handle the "common" processing of responses.
23-
pub fn github(app: &App, url: &str, auth: &Token) -> Result<(Easy, Vec<u8>), curl::Error> {
24-
let url = format!("{}://api.github.com{}", app.config.api_protocol, url);
25-
info!("GITHUB HTTP: {}", url);
26-
27-
let mut headers = List::new();
28-
headers
29-
.append("Accept: application/vnd.github.v3+json")
30-
.unwrap();
31-
headers.append("User-Agent: hello!").unwrap();
32-
headers
33-
.append(&format!("Authorization: token {}", auth.access_token))
34-
.unwrap();
35-
36-
let mut handle = app.handle();
37-
handle.url(&url).unwrap();
38-
handle.get(true).unwrap();
39-
handle.http_headers(headers).unwrap();
40-
41-
let mut data = Vec::new();
42-
{
43-
let mut transfer = handle.transfer();
44-
transfer
45-
.write_function(|buf| {
46-
data.extend_from_slice(buf);
47-
Ok(buf.len())
48-
})
49-
.unwrap();
50-
transfer.perform()?;
51-
}
52-
Ok((handle, data))
53-
}
54-
55-
/// Checks for normal responses
56-
pub fn parse_github_response<'de, 'a: 'de, T: Deserialize<'de>>(
57-
mut resp: Easy,
58-
data: &'a [u8],
59-
) -> CargoResult<T> {
60-
match resp.response_code().unwrap() {
61-
200 => {}
62-
// Ok!
63-
403 => {
64-
return Err(human(
65-
"It looks like you don't have permission \
66-
to query a necessary property from Github \
67-
to complete this request. \
68-
You may need to re-authenticate on \
69-
crates.io to grant permission to read \
70-
github org memberships. Just go to \
71-
https://crates.io/login",
72-
));
73-
}
74-
n => {
75-
let resp = String::from_utf8_lossy(data);
76-
return Err(internal(&format_args!(
77-
"didn't get a 200 result from \
78-
github, got {} with: {}",
79-
n,
80-
resp
81-
)));
82-
}
83-
}
84-
85-
let json = str::from_utf8(data)
86-
.ok()
87-
.chain_error(|| internal("github didn't send a utf8-response"))?;
88-
89-
serde_json::from_str(json).chain_error(|| internal("github didn't send a valid json response"))
90-
}
91-
92-
/// Gets a token with the given string as the access token, but all
93-
/// other info null'd out. Generally, just to be fed to the `github` fn.
94-
pub fn token(token: String) -> Token {
95-
Token {
96-
access_token: token,
97-
scopes: Vec::new(),
98-
token_type: String::new(),
99-
}
100-
}
101-
10212
#[derive(Clone, Debug)]
10313
pub struct SecurityHeadersMiddleware {
10414
headers: HashMap<String, Vec<String>>,
File renamed without changes.

src/lib.rs

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ use util::{R404, C, R};
7575

7676
pub mod app;
7777
pub mod badge;
78-
pub mod categories;
78+
pub mod boot;
7979
pub mod category;
8080
pub mod config;
8181
pub mod crate_owner_invitation;
@@ -84,6 +84,7 @@ pub mod dependency;
8484
pub mod dist;
8585
pub mod download;
8686
pub mod git;
87+
pub mod github;
8788
pub mod http;
8889
pub mod keyword;
8990
pub mod krate;
@@ -137,11 +138,25 @@ pub enum Replica {
137138
pub fn middleware(app: Arc<App>) -> MiddlewareBuilder {
138139
let mut api_router = RouteBuilder::new();
139140

141+
// Route used by both `cargo search` and the frontend
140142
api_router.get("/crates", C(krate::index));
141-
api_router.get("/crates/:crate_id", C(krate::show));
143+
144+
// Routes used by `cargo`
142145
api_router.put("/crates/new", C(krate::new));
143-
api_router.get("/crates/:crate_id/:version", C(version::show));
146+
api_router.get("/crates/:crate_id/owners", C(krate::owners));
147+
api_router.put("/crates/:crate_id/owners", C(krate::add_owners));
148+
api_router.delete("/crates/:crate_id/owners", C(krate::remove_owners));
149+
api_router.delete("/crates/:crate_id/:version/yank", C(version::yank));
150+
api_router.put("/crates/:crate_id/:version/unyank", C(version::unyank));
144151
api_router.get("/crates/:crate_id/:version/download", C(krate::download));
152+
153+
// Routes that appear to be unused
154+
api_router.get("/versions", C(version::index));
155+
api_router.get("/versions/:version_id", C(version::show));
156+
157+
// Routes used by the frontend
158+
api_router.get("/crates/:crate_id", C(krate::show));
159+
api_router.get("/crates/:crate_id/:version", C(version::show));
145160
api_router.get("/crates/:crate_id/:version/readme", C(krate::readme));
146161
api_router.get(
147162
"/crates/:crate_id/:version/dependencies",
@@ -152,27 +167,17 @@ pub fn middleware(app: Arc<App>) -> MiddlewareBuilder {
152167
C(version::downloads),
153168
);
154169
api_router.get("/crates/:crate_id/:version/authors", C(version::authors));
155-
// Used to generate download graphs
156170
api_router.get("/crates/:crate_id/downloads", C(krate::downloads));
157171
api_router.get("/crates/:crate_id/versions", C(krate::versions));
158172
api_router.put("/crates/:crate_id/follow", C(krate::follow));
159173
api_router.delete("/crates/:crate_id/follow", C(krate::unfollow));
160174
api_router.get("/crates/:crate_id/following", C(krate::following));
161-
// This endpoint may now be redundant, check frontend to see if it is
162-
// being used
163-
api_router.get("/crates/:crate_id/owners", C(krate::owners));
164175
api_router.get("/crates/:crate_id/owner_team", C(krate::owner_team));
165176
api_router.get("/crates/:crate_id/owner_user", C(krate::owner_user));
166-
api_router.put("/crates/:crate_id/owners", C(krate::add_owners));
167-
api_router.delete("/crates/:crate_id/owners", C(krate::remove_owners));
168-
api_router.delete("/crates/:crate_id/:version/yank", C(version::yank));
169-
api_router.put("/crates/:crate_id/:version/unyank", C(version::unyank));
170177
api_router.get(
171178
"/crates/:crate_id/reverse_dependencies",
172179
C(krate::reverse_dependencies),
173180
);
174-
api_router.get("/versions", C(version::index));
175-
api_router.get("/versions/:version_id", C(version::show));
176181
api_router.get("/keywords", C(keyword::index));
177182
api_router.get("/keywords/:keyword_id", C(keyword::show));
178183
api_router.get("/categories", C(category::index));

src/owner.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use diesel::prelude::*;
22

33
use app::App;
4-
use http;
4+
use github;
55
use schema::*;
66
use util::{human, CargoResult};
77
use {Crate, User};
@@ -147,7 +147,7 @@ impl Team {
147147
/// Tries to create a Github Team from scratch. Assumes `org` and `team` are
148148
/// correctly parsed out of the full `name`. `name` is passed as a
149149
/// convenience to avoid rebuilding it.
150-
pub fn create_github_team(
150+
fn create_github_team(
151151
app: &App,
152152
conn: &PgConnection,
153153
login: &str,
@@ -184,9 +184,9 @@ impl Team {
184184
// FIXME: we just set per_page=100 and don't bother chasing pagination
185185
// links. A hundred teams should be enough for any org, right?
186186
let url = format!("/orgs/{}/teams?per_page=100", org_name);
187-
let token = http::token(req_user.gh_access_token.clone());
188-
let (handle, data) = http::github(app, &url, &token)?;
189-
let teams: Vec<GithubTeam> = http::parse_github_response(handle, &data)?;
187+
let token = github::token(req_user.gh_access_token.clone());
188+
let (handle, data) = github::github(app, &url, &token)?;
189+
let teams: Vec<GithubTeam> = github::parse_github_response(handle, &data)?;
190190

191191
let team = teams
192192
.into_iter()
@@ -209,8 +209,8 @@ impl Team {
209209
}
210210

211211
let url = format!("/orgs/{}", org_name);
212-
let (handle, resp) = http::github(app, &url, &token)?;
213-
let org: Org = http::parse_github_response(handle, &resp)?;
212+
let (handle, resp) = github::github(app, &url, &token)?;
213+
let org: Org = github::parse_github_response(handle, &resp)?;
214214

215215
NewTeam::new(login, team.id, team.name, org.avatar_url).create_or_update(conn)
216216
}
@@ -276,15 +276,15 @@ fn team_with_gh_id_contains_user(app: &App, github_id: i32, user: &User) -> Carg
276276
}
277277

278278
let url = format!("/teams/{}/memberships/{}", &github_id, &user.gh_login);
279-
let token = http::token(user.gh_access_token.clone());
280-
let (mut handle, resp) = http::github(app, &url, &token)?;
279+
let token = github::token(user.gh_access_token.clone());
280+
let (mut handle, resp) = github::github(app, &url, &token)?;
281281

282282
// Officially how `false` is returned
283283
if handle.response_code().unwrap() == 404 {
284284
return Ok(false);
285285
}
286286

287-
let membership: Membership = http::parse_github_response(handle, &resp)?;
287+
let membership: Membership = github::parse_github_response(handle, &resp)?;
288288

289289
// There is also `state: pending` for which we could possibly give
290290
// some feedback, but it's not obvious how that should work.

src/tests/categories.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ fn select_slugs(conn: &PgConnection) -> Vec<String> {
6060
fn sync_adds_new_categories() {
6161
let conn = pg_connection();
6262

63-
::cargo_registry::categories::sync_with_connection(ALGORITHMS_AND_SUCH, &conn).unwrap();
63+
::cargo_registry::boot::categories::sync_with_connection(ALGORITHMS_AND_SUCH, &conn).unwrap();
6464

6565
let categories = select_slugs(&conn);
6666
assert_eq!(categories, vec!["algorithms", "algorithms::such"]);
@@ -70,8 +70,8 @@ fn sync_adds_new_categories() {
7070
fn sync_removes_missing_categories() {
7171
let conn = pg_connection();
7272

73-
::cargo_registry::categories::sync_with_connection(ALGORITHMS_AND_SUCH, &conn).unwrap();
74-
::cargo_registry::categories::sync_with_connection(ALGORITHMS, &conn).unwrap();
73+
::cargo_registry::boot::categories::sync_with_connection(ALGORITHMS_AND_SUCH, &conn).unwrap();
74+
::cargo_registry::boot::categories::sync_with_connection(ALGORITHMS, &conn).unwrap();
7575

7676
let categories = select_slugs(&conn);
7777
assert_eq!(categories, vec!["algorithms"]);
@@ -81,8 +81,9 @@ fn sync_removes_missing_categories() {
8181
fn sync_adds_and_removes() {
8282
let conn = pg_connection();
8383

84-
::cargo_registry::categories::sync_with_connection(ALGORITHMS_AND_SUCH, &conn).unwrap();
85-
::cargo_registry::categories::sync_with_connection(ALGORITHMS_AND_ANOTHER, &conn).unwrap();
84+
::cargo_registry::boot::categories::sync_with_connection(ALGORITHMS_AND_SUCH, &conn).unwrap();
85+
::cargo_registry::boot::categories::sync_with_connection(ALGORITHMS_AND_ANOTHER, &conn)
86+
.unwrap();
8687

8788
let categories = select_slugs(&conn);
8889
assert_eq!(categories, vec!["algorithms", "another"]);

0 commit comments

Comments
 (0)