Skip to content

Commit

Permalink
Add resource name parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
tinrab committed Nov 12, 2023
1 parent d494d05 commit 7e87489
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 7 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
# Common utilities for Rust
# Bomboni: Common Utilities Library for Rust
13 changes: 8 additions & 5 deletions bomboni/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bomboni"
version = "0.1.1"
version = "0.1.2"
authors = ["Tin Rabzelj <tin@flinect.com>"]
description = "Common utilities for Rust"
repository = "https://github.com/tinrab/bomboni"
Expand All @@ -9,15 +9,18 @@ license-file = "../LICENSE"
readme = "../README.md"
edition = "2021"

exclude = [".github", ".editorconfig", "develop.sh"]

[lib]
name = "bomboni"
path = "src/lib.rs"

[features]
default = ["macros"]
macros = ["dep:regex"]
default = ["macros", "request"]
macros = ["dep:bomboni_derive", "dep:regex"]
request = []

[dependencies]
bomboni_derive = { version = "*", path = "../bomboni_derive", optional = true }
regex = { version = "1.10.2", optional = true }

[dev-dependencies]
bomboni_derive = { path = "../bomboni_derive" }
3 changes: 3 additions & 0 deletions bomboni/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@

#[cfg(feature = "macros")]
pub mod macros;

#[cfg(feature = "request")]
pub mod request;
2 changes: 1 addition & 1 deletion bomboni/src/macros/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! # Common macros for Rust.
//! # Common macros.
//!

//! A collection of common macros for Rust.
Expand Down
3 changes: 3 additions & 0 deletions bomboni/src/request/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
//! # Utilities for working with API requests.

pub mod resource;
34 changes: 34 additions & 0 deletions bomboni/src/request/resource.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//! # Tools for working with API resources.

#[cfg(feature = "macros")]
pub use bomboni_derive::parse_resource_name;

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn parse_names() {
let f = parse_resource_name!([
"users" => u32,
"projects" => u64,
"revisions" => Option<String>,
]);

let (user_id, project_id, revision_id) = f("users/3/projects/5/revisions/1337").unwrap();
assert_eq!(user_id, 3);
assert_eq!(project_id, 5);
assert_eq!(revision_id, Some("1337".to_string()));

let (user_id, project_id, revision_id) = f("users/3/projects/5").unwrap();
assert_eq!(user_id, 3);
assert_eq!(project_id, 5);
assert!(revision_id.is_none());

assert!(parse_resource_name!([
"a" => u32,
"b" => u32,
])("a/1/b/1/c/1")
.is_none());
}
}
21 changes: 21 additions & 0 deletions bomboni_derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "bomboni_derive"
version = "0.1.2"
authors = ["Tin Rabzelj <tin@flinect.com>"]
description = "Macros for Bomboni library"
repository = "https://github.com/tinrab/bomboni"
homepage = "https://github.com/tinrab/bomboni"
license-file = "../LICENSE"
readme = "../README.md"
edition = "2021"

[lib]
name = "bomboni_derive"
path = "src/lib.rs"
proc-macro = true

[dependencies]
proc-macro2 = { version = "1.0.69", features = ["proc-macro"] }
syn = "2.0.39"
quote = "1.0.33"
darling = "0.20.3"
116 changes: 116 additions & 0 deletions bomboni_derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
//! # A procedural macro crate for the Bomboni library.

use proc_macro::TokenStream;
use proc_macro2::{Literal, Span};
use quote::quote;
use syn::parse::{Parse, ParseStream, Result};
use syn::parse_macro_input;
use syn::punctuated::Punctuated;
use syn::{token, Token, Type};

use crate::utility::is_option_type;

mod utility;

#[derive(Debug)]
struct Resource {
_bracket_token: token::Bracket,
segments: Punctuated<Segment, Token![,]>,
}

#[derive(Debug)]
struct Segment {
span: Span,
name: Literal,
_arrow_token: Token![=>],
ty: Type,
}

/// A procedural macro that generates a function that parses a resource name into a tuple of typed segments.
/// Resource name format is documented in Google's AIP [1].
/// Ending segments can also be optional [2].
///
/// [1]: https://google.aip.dev/122
/// [2]: https://google.aip.dev/162
///
/// # Examples
///
/// ```
/// # use bomboni_derive::parse_resource_name;
/// let name = "users/42/projects/1337";
/// let parsed = parse_resource_name!([
/// "users" => u32,
/// "projects" => u64,
/// ])(name);
/// assert_eq!(parsed, Some((42, 1337)));
/// ```
#[proc_macro]
pub fn parse_resource_name(input: TokenStream) -> TokenStream {
let resource = parse_macro_input!(input as Resource);

let mut parse_segments = quote!();
let mut had_optional = false;
for segment in resource.segments.iter() {
let name = &segment.name;
let ty = &segment.ty;
if is_option_type(ty) {
had_optional = true;
parse_segments.extend(quote! {{
if segments_iter.peek() == Some(&#name) {
segments_iter.next()?;
segments_iter.next().map(|e| e.parse().ok()).flatten()
} else {
None
}
}});
} else {
if had_optional {
return syn::Error::new(segment.span, "only ending segments can be optional")
.to_compile_error()
.into();
}
parse_segments.extend(quote! {{
if segments_iter.next()? != #name {
return None;
}
segments_iter.next()?.parse::<#ty>().ok()?
}});
}
parse_segments.extend(quote! {,});
}

quote! {
|name: &str| {
let segments = name.trim().split('/').collect::<Vec<_>>();
let mut segments_iter = segments.into_iter().peekable();
let result = (#parse_segments);
// No extra segments allowed.
if segments_iter.next().is_some() {
return None;
}
Some(result)
}
}
.into()
}

impl Parse for Resource {
fn parse(input: ParseStream) -> Result<Self> {
let content;
Ok(Resource {
_bracket_token: syn::bracketed!(content in input),
segments: content.parse_terminated(Segment::parse, Token![,])?,
})
}
}

impl Parse for Segment {
fn parse(input: ParseStream) -> Result<Self> {
Ok(Segment {
span: input.span(),
name: input.parse()?,
_arrow_token: input.parse()?,
ty: input.parse()?,
})
}
}
12 changes: 12 additions & 0 deletions bomboni_derive/src/utility.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use syn::Type;

pub(crate) fn is_option_type(ty: &Type) -> bool {
if let Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.first() {
if segment.ident == "Option" {
return true;
}
}
}
false
}

0 comments on commit 7e87489

Please sign in to comment.