#[cfg(feature = "http-client")]
mod call;
#[cfg(feature = "http-client")]
pub use call::{WireError, call};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WireField {
pub rust_name: &'static str,
pub wire_name: &'static str,
pub ty: &'static str,
pub required: bool,
}
pub trait WireShape {
const TYPE_NAME: &'static str;
const SERIALIZED: &'static [WireField];
const DESERIALIZED: &'static [WireField];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct NoBody;
impl WireShape for NoBody {
const TYPE_NAME: &'static str = "NoBody";
const SERIALIZED: &'static [WireField] = &[];
const DESERIALIZED: &'static [WireField] = &[];
}
impl<T: WireShape> WireShape for Vec<T> {
const TYPE_NAME: &'static str = <T as WireShape>::TYPE_NAME;
const SERIALIZED: &'static [WireField] = <T as WireShape>::SERIALIZED;
const DESERIALIZED: &'static [WireField] = <T as WireShape>::DESERIALIZED;
}
pub trait Endpoint {
type Request: WireShape + serde::Serialize + Send + Sync;
type Response: WireShape + serde::de::DeserializeOwned + Send;
const SERVICE: &'static str;
const NAME: &'static str;
const METHOD: &'static str;
const PATH: &'static str;
const HAS_BODY: bool;
const REQUEST_FIELDS: &'static [WireField] = <Self::Request as WireShape>::DESERIALIZED;
const REQUEST_SENT_FIELDS: &'static [WireField] = <Self::Request as WireShape>::SERIALIZED;
const RESPONSE_FIELDS: &'static [WireField] = <Self::Response as WireShape>::SERIALIZED;
}
#[derive(Debug, Clone, Copy)]
pub struct ClientEndpoint {
pub method: &'static str,
pub response: &'static [WireField],
pub request: &'static [WireField],
pub request_sent: &'static [WireField],
}
const fn entry_of<'a>(table: &'a [ClientEndpoint], method: &str) -> Option<&'a ClientEndpoint> {
let mut i = 0;
while i < table.len() {
if str_eq(table[i].method, method) {
return Some(&table[i]);
}
i += 1;
}
None
}
#[must_use]
pub const fn client_produces(table: &[ClientEndpoint], method: &str, field: &str) -> bool {
match entry_of(table, method) {
Some(entry) => has_field(entry.response, field),
None => true,
}
}
#[must_use]
pub const fn client_accepts(table: &[ClientEndpoint], method: &str, field: &str) -> bool {
match entry_of(table, method) {
Some(entry) => has_field(entry.request, field),
None => true,
}
}
#[must_use]
pub const fn client_request_covered(
table: &[ClientEndpoint],
method: &str,
supplied: &[&str],
) -> bool {
match entry_of(table, method) {
Some(entry) => omittable_required_covered(entry.request_sent, entry.request, supplied),
None => true,
}
}
#[must_use]
pub const fn has_field(fields: &[WireField], rust_name: &str) -> bool {
let mut i = 0;
while i < fields.len() {
if str_eq(fields[i].rust_name, rust_name) {
return true;
}
i += 1;
}
false
}
#[must_use]
pub const fn omittable_required_covered(
serialized: &[WireField],
deserialized: &[WireField],
supplied: &[&str],
) -> bool {
let mut i = 0;
while i < deserialized.len() {
let field = &deserialized[i];
if field.required {
if !has_field(serialized, field.rust_name) {
return false;
}
if !always_serialized(serialized, field.rust_name)
&& !contains_str(supplied, field.rust_name)
{
return false;
}
}
i += 1;
}
true
}
const fn always_serialized(fields: &[WireField], rust_name: &str) -> bool {
let mut i = 0;
while i < fields.len() {
if str_eq(fields[i].rust_name, rust_name) {
return fields[i].required;
}
i += 1;
}
false
}
#[must_use]
pub const fn path_params_are(template: &str, declared: &[&str]) -> bool {
let bytes = template.as_bytes();
let (mut i, mut seen) = (0, 0);
while i < bytes.len() {
if bytes[i] == b'{' {
let start = i + 1;
let mut end = start;
while end < bytes.len() && bytes[end] != b'}' {
end += 1;
}
if end >= bytes.len() {
return false;
}
if seen >= declared.len() || !str_eq(declared[seen], slice_str(template, start, end)) {
return false;
}
seen += 1;
i = end + 1;
} else {
i += 1;
}
}
seen == declared.len()
}
const fn slice_str(s: &str, start: usize, end: usize) -> &str {
let bytes = s.as_bytes();
let mut i = start;
while i < end {
if bytes[i] >= 0x80 {
return "";
}
i += 1;
}
let (head, _) = s.split_at(end);
let (_, tail) = head.split_at(start);
tail
}
#[must_use]
pub fn render_path(template: &str, params: &[(&str, &str)]) -> String {
let mut out = String::with_capacity(template.len());
let mut rest = template;
while let Some(open) = rest.find('{') {
out.push_str(&rest[..open]);
rest = &rest[open + 1..];
let Some(close) = rest.find('}') else {
out.push('{');
break;
};
let name = &rest[..close];
if let Some((_, value)) = params.iter().find(|(k, _)| *k == name) {
out.push_str(&encode_segment(value));
} else {
out.push('{');
out.push_str(name);
out.push('}');
}
rest = &rest[close + 1..];
}
out.push_str(rest);
out
}
fn encode_segment(value: &str) -> String {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
if value.is_empty() {
return "%20".to_owned();
}
if value == "." {
return "%2E".to_owned();
}
if value == ".." {
return "%2E%2E".to_owned();
}
let mut out = String::with_capacity(value.len());
for byte in value.bytes() {
if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
out.push(byte as char);
} else {
out.push('%');
out.push(HEX[usize::from(byte >> 4)] as char);
out.push(HEX[usize::from(byte & 0x0f)] as char);
}
}
out
}
const fn str_eq(a: &str, b: &str) -> bool {
let (a, b) = (a.as_bytes(), b.as_bytes());
if a.len() != b.len() {
return false;
}
let mut i = 0;
while i < a.len() {
if a[i] != b[i] {
return false;
}
i += 1;
}
true
}
const fn contains_str(haystack: &[&str], needle: &str) -> bool {
let mut i = 0;
while i < haystack.len() {
if str_eq(haystack[i], needle) {
return true;
}
i += 1;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
const FIELDS: &[WireField] = &[
WireField {
rust_name: "name",
wire_name: "name",
ty: "String",
required: true,
},
WireField {
rust_name: "price_cents",
wire_name: "priceCents",
ty: "Option<u32>",
required: false,
},
];
#[test]
fn has_field_matches_the_rust_name_not_the_wire_name() {
assert!(has_field(FIELDS, "price_cents"));
assert!(!has_field(FIELDS, "priceCents"));
assert!(!has_field(FIELDS, "sku"));
}
#[test]
fn has_field_does_not_match_a_prefix() {
assert!(!has_field(FIELDS, "nam"));
assert!(!has_field(FIELDS, "names"));
}
#[test]
fn an_always_serialized_field_never_has_to_be_named_at_a_call_site() {
assert!(omittable_required_covered(FIELDS, FIELDS, &[]));
assert!(omittable_required_covered(&[], &[], &[]));
}
#[test]
fn a_required_field_the_request_may_omit_must_be_named() {
const SER: &[WireField] = &[WireField {
rust_name: "sku",
wire_name: "sku",
ty: "String",
required: false,
}];
const DE: &[WireField] = &[WireField {
rust_name: "sku",
wire_name: "sku",
ty: "String",
required: true,
}];
assert!(!omittable_required_covered(SER, DE, &[]));
assert!(omittable_required_covered(SER, DE, &["sku"]));
}
#[test]
fn a_required_field_the_request_can_never_send_is_uncoverable() {
const DE: &[WireField] = &[WireField {
rust_name: "sku",
wire_name: "sku",
ty: "String",
required: true,
}];
assert!(!omittable_required_covered(&[], DE, &[]));
assert!(!omittable_required_covered(&[], DE, &["sku"]));
}
const _: () = assert!(has_field(FIELDS, "name"));
const _: () = assert!(!has_field(FIELDS, "sku"));
const _: () = assert!(omittable_required_covered(FIELDS, FIELDS, &[]));
#[test]
fn path_params_are_matches_name_and_order() {
assert!(path_params_are("/items/{id}", &["id"]));
assert!(path_params_are("/a/{x}/b/{y}", &["x", "y"]));
assert!(path_params_are("/items", &[]));
assert!(!path_params_are("/a/{x}/b/{y}", &["y", "x"]));
assert!(!path_params_are("/items/{id}", &[]));
assert!(!path_params_are("/items", &["id"]));
assert!(!path_params_are("/items/{sku}", &["id"]));
assert!(!path_params_are("/a/{x}/b/{y", &["x"]));
}
const _: () = assert!(path_params_are("/items/{id}", &["id"]));
const _: () = assert!(!path_params_are("/items/{id}", &["sku"]));
#[test]
fn render_path_percent_encodes_each_segment() {
assert_eq!(render_path("/items/{id}", &[("id", "a b")]), "/items/a%20b");
assert_eq!(
render_path("/items/{id}", &[("id", "../admin")]),
"/items/..%2Fadmin"
);
assert_eq!(
render_path("/items/{id}/reviews", &[("id", "..")]),
"/items/%2E%2E/reviews"
);
assert_eq!(render_path("/items/{id}", &[("id", ".")]), "/items/%2E");
assert_eq!(render_path("/items/{id}", &[("id", "")]), "/items/%20");
assert_eq!(render_path("/items", &[]), "/items");
assert_eq!(
render_path("/a/{x}/b/{y}", &[("x", "1"), ("y", "2")]),
"/a/1/b/2"
);
}
#[test]
fn render_path_leaves_an_unsupplied_placeholder_visible() {
assert_eq!(render_path("/items/{id}", &[]), "/items/{id}");
}
#[test]
fn a_list_carries_its_elements_shape() {
struct Item;
impl WireShape for Item {
const TYPE_NAME: &'static str = "Item";
const SERIALIZED: &'static [WireField] = FIELDS;
const DESERIALIZED: &'static [WireField] = FIELDS;
}
assert_eq!(<Vec<Item> as WireShape>::SERIALIZED.len(), FIELDS.len());
assert_eq!(<Vec<Item> as WireShape>::TYPE_NAME, "Item");
}
#[test]
fn no_body_is_empty_in_both_directions() {
assert!(<NoBody as WireShape>::SERIALIZED.is_empty());
assert!(<NoBody as WireShape>::DESERIALIZED.is_empty());
}
}