Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/@unreleased/pr-480.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type: break
break:
description: Expose conjure tags as endpoint parameters
links:
- https://github.com/palantir/conjure-rust/pull/480
19 changes: 18 additions & 1 deletion conjure-codegen/src/servers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::types::{
use heck::ToUpperCamelCase;
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use std::collections::BTreeSet;

#[derive(Copy, Clone)]
enum Style {
Expand Down Expand Up @@ -168,6 +169,14 @@ fn generate_trait_endpoint(
None => quote!(),
};

let tags = match endpoint.tags().is_empty() {
false => {
let tags = crate::servers::tags(endpoint.tags());
quote!(, tags = #tags)
}
true => quote!(),
};

let auth_arg = auth_arg(endpoint);
let args = endpoint.args().iter().map(|a| arg(ctx, def, endpoint, a));
let request_context_arg = request_context_arg(endpoint);
Expand All @@ -180,7 +189,7 @@ fn generate_trait_endpoint(
// ignore deprecation since the endpoint has to be implemented regardless
quote! {
#docs
#[endpoint(method = #method, path = #path, name = #endpoint_name #produces)]
#[endpoint(method = #method, path = #path, name = #endpoint_name #tags #produces)]
#async_ fn #name(&self #auth_arg #(, #args)* #request_context_arg) -> #ret_ty;
}
}
Expand All @@ -198,6 +207,14 @@ fn produces(ctx: &Context, ty: &Type) -> TokenStream {
}
}

fn tags(tags: &BTreeSet<String>) -> TokenStream {
let tag_items = tags.iter().map(|tag| quote!(#tag));

quote! {
[ #(#tag_items),* ]
}
}

fn auth_arg(endpoint: &EndpointDefinition) -> TokenStream {
match endpoint.auth() {
Some(auth) => {
Expand Down
11 changes: 11 additions & 0 deletions conjure-http/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ pub trait EndpointMetadata {

/// If the endpoint is deprecated, returns the deprecation documentation.
fn deprecated(&self) -> Option<&str>;

/// Tags for additional metadata about the endpoint
fn tags(&self) -> &[&str];
}

impl<T> EndpointMetadata for Box<T>
Expand Down Expand Up @@ -95,6 +98,10 @@ where
fn deprecated(&self) -> Option<&str> {
(**self).deprecated()
}

fn tags(&self) -> &[&str] {
(**self).tags()
}
}

/// A blocking HTTP endpoint.
Expand Down Expand Up @@ -228,6 +235,10 @@ impl<I, O> EndpointMetadata for BoxAsyncEndpoint<'_, I, O> {
fn deprecated(&self) -> Option<&str> {
self.inner.deprecated()
}

fn tags(&self) -> &[&str] {
self.inner.tags()
}
}

impl<I, O> AsyncEndpoint<I, O> for BoxAsyncEndpoint<'_, I, O>
Expand Down
55 changes: 53 additions & 2 deletions conjure-macros/src/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ use crate::{Asyncness, Errors};
use heck::ToUpperCamelCase;
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use std::collections::BTreeSet;
use structmeta::StructMeta;
use syn::spanned::Spanned;
use syn::{
parse_macro_input, Error, FnArg, GenericParam, Generics, ItemTrait, LitStr, Meta, Pat, PatType,
ReturnType, TraitItem, TraitItemFn, Type, Visibility,
parse_macro_input, punctuated::Punctuated, Error, FnArg, GenericParam, Generics, ItemTrait,
LitStr, Meta, Pat, PatType, ReturnType, Token, TraitItem, TraitItemFn, Type, Visibility,
};

pub fn generate(
Expand Down Expand Up @@ -240,6 +241,20 @@ fn generate_endpoint_metadata(service: &Service, endpoint: &Endpoint) -> TokenSt
quote!(#name)
}
};
let tags = match &endpoint.params.tags {
Some(tags) => {
let tag_strings = tags.iter().map(|tag| quote!(#tag));
quote! {
{
static TAGS: &[&str] = &[#(#tag_strings),*];
TAGS
}
}
}
None => quote! {
&[]
},
};

quote! {
impl<T> conjure_http::server::EndpointMetadata for #struct_name<T> {
Expand All @@ -266,6 +281,10 @@ fn generate_endpoint_metadata(service: &Service, endpoint: &Endpoint) -> TokenSt
fn deprecated(&self) -> conjure_http::private::Option<&str> {
conjure_http::private::Option::None
}

fn tags(&self) -> &[&str] {
#tags
}
}
}
}
Expand Down Expand Up @@ -751,6 +770,37 @@ impl Endpoint {
}
}

#[derive(Default, Clone)]
struct Tags(BTreeSet<String>);

impl std::ops::Deref for Tags {
type Target = BTreeSet<String>;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl syn::parse::Parse for Tags {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut tags = BTreeSet::new();

if input.peek(syn::token::Bracket) {
let content;
syn::bracketed!(content in input);
let punctuated: Punctuated<LitStr, Token![,]> = Punctuated::parse_terminated(&content)?;

for lit_str in punctuated {
Comment on lines +791 to +793
Copy link

Copilot AI Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider using a more descriptive variable name than 'punctuated' to improve code readability, such as 'tag_literals' or 'comma_separated_tags'.

Suggested change
let punctuated: Punctuated<LitStr, Token![,]> = Punctuated::parse_terminated(&content)?;
for lit_str in punctuated {
let tag_literals: Punctuated<LitStr, Token![,]> = Punctuated::parse_terminated(&content)?;
for lit_str in tag_literals {

Copilot uses AI. Check for mistakes.
tags.insert(lit_str.value());
}
} else {
let lit_str: LitStr = input.parse()?;
tags.insert(lit_str.value());
}
Ok(Tags(tags))
}
}

#[derive(StructMeta)]
struct ServiceParams {
name: Option<LitStr>,
Expand All @@ -762,6 +812,7 @@ struct EndpointParams {
path: LitStr,
name: Option<LitStr>,
produces: Option<Type>,
tags: Option<Tags>,
}

enum ArgType {
Expand Down
46 changes: 46 additions & 0 deletions conjure-test/src/test/servers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1159,3 +1159,49 @@ fn custom_config() {
assert_eq!(endpoints[0].service_name(), "service_name");
assert_eq!(endpoints[0].name(), "name");
}

#[test]
fn test_endpoint_tags() {
use self::test_service::TestServiceEndpoints;

let handler = TestServiceHandler::new();
let endpoints: Vec<Box<dyn Endpoint<RemoteBody, Vec<u8>> + Sync + Send>> =
TestServiceEndpoints::new(handler).endpoints(&Arc::new(ConjureRuntime::new()));

let context_endpoint = endpoints.iter().find(|e| e.name() == "context").unwrap();
let context_tags = context_endpoint.tags();
assert_eq!(context_tags.len(), 1);
assert_eq!(context_tags[0], "server-request-context");

let context_no_args_endpoint = endpoints
.iter()
.find(|e| e.name() == "contextNoArgs")
.unwrap();
let context_no_args_tags = context_no_args_endpoint.tags();
assert_eq!(context_no_args_tags.len(), 1);
assert_eq!(context_no_args_tags[0], "server-request-context");

let small_request_body_endpoint = endpoints
.iter()
.find(|e| e.name() == "smallRequestBody")
.unwrap();
let small_request_body_tags = small_request_body_endpoint.tags();
assert_eq!(small_request_body_tags.len(), 1);
assert_eq!(small_request_body_tags[0], "server-limit-request-size: 10b");

let small_request_body_endpoint = endpoints
.iter()
.find(|e| e.name() == "streamingRequest")
.unwrap();
let small_request_body_tags = small_request_body_endpoint.tags();
assert_eq!(small_request_body_tags.len(), 2);
assert!(small_request_body_tags.contains(&"no-response-compression"));
assert!(small_request_body_tags.contains(&"server-track-allocations"));

let query_params_endpoint = endpoints
.iter()
.find(|e| e.name() == "queryParams")
.unwrap();
let query_params_tags = query_params_endpoint.tags();
assert_eq!(query_params_tags.len(), 0);
}
4 changes: 2 additions & 2 deletions conjure-test/test-ir.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions conjure-test/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ services:
set:
type: set<boolean>
param-type: query
tags:
- new_tag
aliasQueryParams:
http: GET /aliasQueryParams
args:
Expand Down Expand Up @@ -269,6 +271,9 @@ services:
body: OptionalAlias
streamingRequest:
http: POST /streamingRequest
tags:
- no-response-compression
- server-track-allocations
args:
body: binary
streamingAliasRequest:
Expand Down