Skip to content

Commit 27de3c8

Browse files
committed
get_or_create_from_context
1 parent e56ef1f commit 27de3c8

File tree

4 files changed

+91
-25
lines changed

4 files changed

+91
-25
lines changed

lib/api_run/src/run.rs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
use bencher_endpoint::{CorsResponse, Endpoint, Post, ResponseCreated};
2-
use bencher_json::{
3-
project, system::auth, JsonNewOrganization, JsonNewRun, JsonReport, NameIdKind, ResourceName,
4-
RunContext,
5-
};
2+
use bencher_json::{JsonNewRun, JsonReport, RunContext};
63
use bencher_rbac::project::Permission;
74
use bencher_schema::{
8-
conn_lock,
95
context::ApiContext,
106
error::{bad_request_error, forbidden_error, issue_error},
117
model::{
12-
organization::QueryOrganization,
138
project::{report::QueryReport, QueryProject},
149
user::auth::{AuthUser, PubBearerToken},
1510
},
@@ -61,15 +56,25 @@ async fn post_inner(
6156
(auth_user, query_project)
6257
},
6358
(Some(auth_user), None) => {
59+
let Some(project_name) = json_run.context.as_ref().and_then(RunContext::name) else {
60+
return Err(bad_request_error(
61+
"The `project` field was not specified nor was a run `context` provided for the name",
62+
));
63+
};
6464
let Some(project_slug) = json_run.context.as_ref().map(RunContext::slug) else {
6565
return Err(bad_request_error(
66-
"The `project` field was not specified nor was a run `context` provided",
66+
"The `project` field was not specified nor was a run `context` provided for the slug",
6767
));
6868
};
69-
let query_project =
70-
QueryProject::get_or_create(log, context, &auth_user, &project_slug.into())
71-
.await
72-
.map_err(|e| forbidden_error(e.to_string()))?;
69+
let query_project = QueryProject::get_or_create_from_context(
70+
log,
71+
context,
72+
&auth_user,
73+
project_name,
74+
project_slug,
75+
)
76+
.await
77+
.map_err(|e| forbidden_error(e.to_string()))?;
7378
(auth_user, query_project)
7479
},
7580
_ => return Err(bad_request_error("Not yet supported")),

lib/bencher_context/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ publish = false
88

99
[features]
1010
client = ["dep:gix", "dep:uuid", "dep:windows"]
11-
server = ["dep:uuid", "bencher_valid/server"]
11+
server = ["dep:uuid", "bencher_valid/server", "uuid/v4"]
1212
schema = ["dep:schemars"]
1313

1414
[dependencies]

lib/bencher_context/src/server/mod.rs

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use bencher_valid::{Slug, MAX_LEN};
1+
use bencher_valid::{ResourceName, Slug};
22
use uuid::Uuid;
33

44
use crate::{ContextPath, RunContext};
@@ -40,14 +40,32 @@ impl RunContext {
4040
self.get(ContextPath::TESTBED_FINGERPRINT)?.parse().ok()
4141
}
4242

43+
pub fn name(&self) -> Option<ResourceName> {
44+
self.repo_name()
45+
.map(truncate_name)
46+
.or_else(|| {
47+
self.repo_hash()
48+
.map(short_hash)
49+
.map(|hash| format!("Project ({hash})"))
50+
})
51+
.or_else(|| {
52+
self.testbed_fingerprint()
53+
.map(base36::encode_uuid)
54+
.as_deref()
55+
.map(short_fingerprint)
56+
.map(|fingerprint| format!("My Project ({fingerprint})"))
57+
})
58+
.unwrap_or_else(|| {
59+
let id = short_fingerprint(&base36::encode_uuid(Uuid::new_v4()));
60+
format!("New Project ({id})")
61+
})
62+
.parse()
63+
.ok()
64+
}
65+
4366
pub fn slug(&self) -> Slug {
44-
// + 42 chars
45-
let name = self.repo_name().map(truncate_name).unwrap_or_default();
46-
// + 1 char (-)
47-
// + 7 chars
67+
let name = self.repo_name().map(short_name).unwrap_or_default();
4868
let hash = self.repo_hash().map(short_hash).unwrap_or_default();
49-
// + 1 char (-)
50-
// + 13 chars
5169
let fingerprint = self
5270
.testbed_fingerprint()
5371
.map(base36::encode_uuid)
@@ -58,19 +76,34 @@ impl RunContext {
5876
// in case any of the values are empty
5977
// they will essentially be ignored
6078
let slug = format!("{name} {hash} {fingerprint}");
61-
debug_assert!(slug.len() <= MAX_LEN, "Slug is too long: {slug}");
79+
debug_assert!(slug.len() <= Slug::MAX_LEN, "Slug is too long: {slug}");
6280
Slug::new(slug)
6381
}
6482
}
6583

6684
fn truncate_name(name: &str) -> String {
67-
name.chars().take(42).collect()
85+
name.chars().take(ResourceName::MAX_LEN).collect()
86+
}
87+
88+
const SHORT_NAME_LEN: usize = 42;
89+
const SHORT_HASH_LEN: usize = 7;
90+
const SHORT_FINGERPRINT_LEN: usize = 13;
91+
#[allow(dead_code)]
92+
const DASH_LEN: usize = 1;
93+
94+
// Statically assert that the sum of the lengths of the short names
95+
// is less than or equal to the maximum length of a slug
96+
const _: [(); SHORT_NAME_LEN + DASH_LEN + SHORT_HASH_LEN + DASH_LEN + SHORT_FINGERPRINT_LEN] =
97+
[(); Slug::MAX_LEN];
98+
99+
fn short_name(name: &str) -> String {
100+
name.chars().take(SHORT_NAME_LEN).collect()
68101
}
69102

70103
fn short_hash(hash: &str) -> String {
71-
hash.chars().take(7).collect()
104+
hash.chars().take(SHORT_HASH_LEN).collect()
72105
}
73106

74107
fn short_fingerprint(fingerprint: &str) -> String {
75-
fingerprint.chars().take(13).collect()
108+
fingerprint.chars().take(SHORT_FINGERPRINT_LEN).collect()
76109
}

lib/bencher_schema/src/model/project/mod.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ use crate::{
1515
conn_lock,
1616
context::{DbConnection, Rbac},
1717
error::{
18-
assert_parentage, forbidden_error, resource_conflict_err, resource_not_found_error,
19-
unauthorized_error, BencherResource,
18+
assert_parentage, forbidden_error, resource_conflict_err, resource_not_found_err,
19+
resource_not_found_error, unauthorized_error, BencherResource,
2020
},
2121
macros::{
2222
fn_get::{fn_from_uuid, fn_get, fn_get_uuid},
@@ -74,6 +74,13 @@ impl QueryProject {
7474
Project
7575
);
7676

77+
fn from_slug(conn: &mut DbConnection, slug: &Slug) -> Result<Self, HttpError> {
78+
schema::project::table
79+
.filter(schema::project::slug.eq(slug))
80+
.first(conn)
81+
.map_err(resource_not_found_err!(Project, slug.clone()))
82+
}
83+
7784
pub async fn get_or_create(
7885
log: &Logger,
7986
context: &ApiContext,
@@ -105,6 +112,27 @@ impl QueryProject {
105112
Self::create(log, context, auth_user, &query_organization, json_project).await
106113
}
107114

115+
pub async fn get_or_create_from_context(
116+
log: &Logger,
117+
context: &ApiContext,
118+
auth_user: &AuthUser,
119+
project_name: ResourceName,
120+
project_slug: Slug,
121+
) -> Result<Self, HttpError> {
122+
if let Ok(query_project) = Self::from_slug(conn_lock!(context), &project_slug) {
123+
return Ok(query_project);
124+
}
125+
126+
let query_organization = QueryOrganization::get_or_create(context, auth_user).await?;
127+
let json_project = JsonNewProject {
128+
name: project_name,
129+
slug: Some(project_slug),
130+
url: None,
131+
visibility: None,
132+
};
133+
Self::create(log, context, auth_user, &query_organization, json_project).await
134+
}
135+
108136
pub async fn create(
109137
log: &Logger,
110138
context: &ApiContext,

0 commit comments

Comments
 (0)