Skip to content

Commit 4fd5271

Browse files
authored
Support @docs directive (#19)
1 parent 1bc1c33 commit 4fd5271

File tree

5 files changed

+84
-11
lines changed

5 files changed

+84
-11
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use serde_json::Value;
2+
3+
use crate::{Directive, DirectiveParams, FromDirective};
4+
5+
#[derive(Debug, Clone, Default)]
6+
pub struct Docs {
7+
pub deprecated: bool,
8+
pub hidden: bool,
9+
pub tag: Option<String>,
10+
}
11+
12+
impl FromDirective for Docs {
13+
fn from_directive(directive: Directive) -> Result<Self, String> {
14+
if directive.name != "docs" {
15+
return Err(format!(
16+
"Expected 'docs' directive, got '{}'",
17+
directive.name
18+
));
19+
}
20+
21+
let mut docs = Docs::default();
22+
23+
if let DirectiveParams::KeyValue(params) = &directive.params {
24+
// Parse deprecated flag
25+
if let Some(Value::Bool(value)) = params.get("deprecated") {
26+
docs.deprecated = *value;
27+
} else if params.contains_key("deprecated") {
28+
return Err("'deprecated' parameter must be a boolean".to_string());
29+
}
30+
31+
// Parse hidden flag
32+
if let Some(Value::Bool(value)) = params.get("hidden") {
33+
docs.hidden = *value;
34+
} else if params.contains_key("hidden") {
35+
return Err("'hidden' parameter must be a boolean".to_string());
36+
}
37+
38+
// Parse tag
39+
if let Some(Value::String(value)) = params.get("tag") {
40+
docs.tag = Some(value.clone());
41+
} else if params.contains_key("tag") {
42+
return Err("'tag' parameter must be a string".to_string());
43+
}
44+
45+
// Check for unknown parameters
46+
for key in params.keys() {
47+
match key.as_str() {
48+
"deprecated" | "hidden" | "tag" => {}
49+
_ => return Err(format!("Unknown parameter '{}' for @docs directive", key)),
50+
}
51+
}
52+
} else {
53+
return Err("@docs directive requires key-value parameters".to_string());
54+
}
55+
56+
Ok(docs)
57+
}
58+
}

aiscript-directive/src/route/mod.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
use docs::Docs;
12
use serde_json::Value;
23

4+
mod docs;
35
use crate::{Directive, FromDirective};
46

5-
#[derive(Debug, Copy, Clone, Default)]
7+
#[derive(Debug, Clone, Default)]
68
pub struct RouteAnnotation {
79
pub auth: Auth,
8-
pub deprecated: bool,
10+
pub docs: Option<Docs>,
911
pub sso_provider: Option<SsoProvider>,
1012
}
1113

@@ -62,12 +64,12 @@ impl RouteAnnotation {
6264
matches!(self.auth, Auth::Jwt)
6365
}
6466

65-
pub fn or(mut self, other: RouteAnnotation) -> Self {
67+
pub fn or(mut self, other: &RouteAnnotation) -> Self {
6668
if matches!(self.auth, Auth::None) {
6769
self.auth = other.auth;
6870
}
69-
if !self.deprecated {
70-
self.deprecated = other.deprecated
71+
if self.docs.is_none() {
72+
self.docs = other.docs.clone()
7173
}
7274
self
7375
}
@@ -93,11 +95,11 @@ impl RouteAnnotation {
9395
return Err("Duplicate auth directive".into());
9496
}
9597
}
96-
"deprecated" => {
97-
if self.deprecated {
98-
return Err("Duplicate deprecated directive".into());
98+
"docs" => {
99+
if self.docs.is_some() {
100+
return Err("Duplicate @docs directive".into());
99101
} else {
100-
self.deprecated = true;
102+
self.docs = Some(Docs::from_directive(directive)?);
101103
}
102104
}
103105
"sso" => {

aiscript-runtime/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ async fn run_server(
209209
let mut r = Router::new();
210210
for endpoint_spec in route.endpoints {
211211
let endpoint = Endpoint {
212-
annotation: endpoint_spec.annotation.or(route.annotation),
212+
annotation: endpoint_spec.annotation.or(&route.annotation),
213213
query_params: endpoint_spec.query.into_iter().map(convert_field).collect(),
214214
body_type: endpoint_spec.body.kind,
215215
body_fields: endpoint_spec

aiscript-runtime/src/openapi/mod.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ impl OpenAPIGenerator {
4646
} else {
4747
format!("{}/{}", path, path_spec.path)
4848
};
49+
// Skip hidden routes if applicable
50+
if route.annotation.docs.as_ref().is_some_and(|d| d.hidden) {
51+
continue;
52+
}
4953
paths.insert(full_path.replace("//", "/"), path_item);
5054
}
5155
}
@@ -153,12 +157,19 @@ impl OpenAPIGenerator {
153157

154158
fn create_operation(route: &Route, endpoint: &Endpoint, path_spec: &PathSpec) -> Operation {
155159
let route_name = route.prefix.trim_matches('/').to_string();
156-
let tags = if route_name.is_empty() {
160+
let mut tags = if route_name.is_empty() {
157161
vec!["default".to_string()]
158162
} else {
159163
vec![route_name]
160164
};
161165

166+
// Add custom tag from docs directive if present
167+
if let Some(docs) = &route.annotation.docs {
168+
if let Some(tag) = &docs.tag {
169+
tags = vec![tag.clone()];
170+
}
171+
}
172+
162173
let mut parameters = Self::create_path_parameters(&path_spec.params);
163174
for query_field in &endpoint.query {
164175
parameters.push(Self::create_query_parameter(query_field));
@@ -185,6 +196,7 @@ impl OpenAPIGenerator {
185196
parameters,
186197
request_body,
187198
responses: Some(Self::create_default_responses()),
199+
deprecated: route.annotation.docs.as_ref().map(|d| d.deprecated),
188200
// security: Self::get_security_requirement(endpoint.annotation),
189201
..Default::default()
190202
}

examples/routes/ai.ai

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
@docs(tag="Guess", deprecated=true)
12
get /guess {
23

34
query {

0 commit comments

Comments
 (0)