mod project;
mod settle;
use std::sync::Arc;
pub(crate) use project::Project;
pub(crate) use settle::{Consumer, Settlement};
use crate::error::PubSubError;
use crate::subscriber::PubSubSubscriber;
use crate::subscription::GooglePubSub;
pub(crate) fn subscribe(
project: &Arc<Project>,
descriptor: &GooglePubSub,
) -> Result<PubSubSubscriber, PubSubError> {
project.ensure_open()?;
let consumer = project.open(descriptor)?;
Ok(PubSubSubscriber::in_process(
project.subscription_path(descriptor.subscription()),
consumer,
descriptor.batch_wait_value(),
))
}
pub(crate) mod limits {
pub(crate) const ATTRIBUTES: usize = 100;
pub(crate) const ATTRIBUTE_KEY: usize = 256;
pub(crate) const ATTRIBUTE_VALUE: usize = 1024;
pub(crate) const ORDERING_KEY: usize = 1024;
pub(crate) const MESSAGE: usize = 10 * 1024 * 1024;
}
fn check_project(project: &str) -> Result<(), String> {
let (domain_ok, id) = project
.rsplit_once(':')
.map_or((true, project), |(domain, id)| {
let domain_ok = !domain.is_empty()
&& domain.chars().all(|c| {
c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '.' | '-')
});
(domain_ok, id)
});
let id_ok = (6..=30).contains(&id.len())
&& id.starts_with(|c: char| c.is_ascii_lowercase())
&& !id.ends_with('-')
&& id
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-');
if domain_ok && id_ok {
Ok(())
} else {
Err(format!(
"the project {project:?}, which is not a project id"
))
}
}
pub(crate) fn check_resource_name(collection: &str, name: &str) -> Result<(), String> {
let id = match name.strip_prefix("projects/") {
Some(rest) => {
let mut parts = rest.splitn(3, '/');
let (project, kind, id) = (parts.next(), parts.next(), parts.next());
match (project, kind, id) {
(Some(project), Some(kind), Some(id)) if kind == collection => {
check_project(project).map_err(|reason| format!("{name:?} names {reason}"))?;
id
}
_ => {
return Err(format!(
"{name:?} is not a resource name of the form \
projects/{{project}}/{collection}/{{id}}"
));
}
}
}
None => name,
};
let valid_char =
|c: char| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '~' | '+' | '%');
if !(3..=255).contains(&id.len())
|| !id.starts_with(|c: char| c.is_ascii_alphabetic())
|| !id.chars().all(valid_char)
|| id.starts_with("goog")
{
return Err(format!(
"{id:?} is not a valid resource id: 3 to 255 characters, starting with a letter, of \
letters, digits and -_.~+%, and not starting with \"goog\""
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::check_resource_name;
#[test]
fn the_service_accepts_these_names() {
for name in [
"orders",
"orders-workers",
"conformance.lifecycle.1f-2a-0",
"a~b+c%d_e",
"projects/my-project/topics/orders",
"projects/example.com:my-project/topics/orders",
] {
assert!(check_resource_name("topics", name).is_ok(), "{name}");
}
}
#[test]
fn the_service_refuses_these_names() {
for name in [
"",
"ab",
"1orders",
"orders workers",
"orders/eu",
"google-orders",
"projects/my-project/subscriptions/orders",
"projects//topics/orders",
"projects/!/topics/orders",
"projects/short/topics/orders",
"projects/My-Project/topics/orders",
"projects/my-project-/topics/orders",
"projects/:my-project/topics/orders",
"projects/my-project/topics",
] {
assert!(check_resource_name("topics", name).is_err(), "{name}");
}
assert!(check_resource_name("topics", &"o".repeat(256)).is_err());
}
}