Skip to content
Merged
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
58 changes: 58 additions & 0 deletions aiscript-directive/src/route/docs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use serde_json::Value;

use crate::{Directive, DirectiveParams, FromDirective};

#[derive(Debug, Clone, Default)]
pub struct Docs {
pub deprecated: bool,
pub hidden: bool,
pub tag: Option<String>,
}

impl FromDirective for Docs {
fn from_directive(directive: Directive) -> Result<Self, String> {
if directive.name != "docs" {
return Err(format!(
"Expected 'docs' directive, got '{}'",
directive.name
));
}

let mut docs = Docs::default();

if let DirectiveParams::KeyValue(params) = &directive.params {
// Parse deprecated flag
if let Some(Value::Bool(value)) = params.get("deprecated") {
docs.deprecated = *value;
} else if params.contains_key("deprecated") {
return Err("'deprecated' parameter must be a boolean".to_string());
}

// Parse hidden flag
if let Some(Value::Bool(value)) = params.get("hidden") {
docs.hidden = *value;
} else if params.contains_key("hidden") {
return Err("'hidden' parameter must be a boolean".to_string());
}

// Parse tag
if let Some(Value::String(value)) = params.get("tag") {
docs.tag = Some(value.clone());
} else if params.contains_key("tag") {
return Err("'tag' parameter must be a string".to_string());
}

// Check for unknown parameters
for key in params.keys() {
match key.as_str() {
"deprecated" | "hidden" | "tag" => {}
_ => return Err(format!("Unknown parameter '{}' for @docs directive", key)),
}
}
} else {
return Err("@docs directive requires key-value parameters".to_string());
}

Ok(docs)
}
}
20 changes: 11 additions & 9 deletions aiscript-directive/src/route/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use docs::Docs;
use serde_json::Value;

mod docs;
use crate::{Directive, FromDirective};

#[derive(Debug, Copy, Clone, Default)]
#[derive(Debug, Clone, Default)]
pub struct RouteAnnotation {
pub auth: Auth,
pub deprecated: bool,
pub docs: Option<Docs>,
pub sso_provider: Option<SsoProvider>,
}

Expand Down Expand Up @@ -62,12 +64,12 @@ impl RouteAnnotation {
matches!(self.auth, Auth::Jwt)
}

pub fn or(mut self, other: RouteAnnotation) -> Self {
pub fn or(mut self, other: &RouteAnnotation) -> Self {
if matches!(self.auth, Auth::None) {
self.auth = other.auth;
}
if !self.deprecated {
self.deprecated = other.deprecated
if self.docs.is_none() {
self.docs = other.docs.clone()
}
self
}
Expand All @@ -93,11 +95,11 @@ impl RouteAnnotation {
return Err("Duplicate auth directive".into());
}
}
"deprecated" => {
if self.deprecated {
return Err("Duplicate deprecated directive".into());
"docs" => {
if self.docs.is_some() {
return Err("Duplicate @docs directive".into());
} else {
self.deprecated = true;
self.docs = Some(Docs::from_directive(directive)?);
}
}
"sso" => {
Expand Down
2 changes: 1 addition & 1 deletion aiscript-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ async fn run_server(
let mut r = Router::new();
for endpoint_spec in route.endpoints {
let endpoint = Endpoint {
annotation: endpoint_spec.annotation.or(route.annotation),
annotation: endpoint_spec.annotation.or(&route.annotation),
query_params: endpoint_spec.query.into_iter().map(convert_field).collect(),
body_type: endpoint_spec.body.kind,
body_fields: endpoint_spec
Expand Down
14 changes: 13 additions & 1 deletion aiscript-runtime/src/openapi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ impl OpenAPIGenerator {
} else {
format!("{}/{}", path, path_spec.path)
};
// Skip hidden routes if applicable
if route.annotation.docs.as_ref().is_some_and(|d| d.hidden) {
continue;
}
paths.insert(full_path.replace("//", "/"), path_item);
}
}
Expand Down Expand Up @@ -153,12 +157,19 @@ impl OpenAPIGenerator {

fn create_operation(route: &Route, endpoint: &Endpoint, path_spec: &PathSpec) -> Operation {
let route_name = route.prefix.trim_matches('/').to_string();
let tags = if route_name.is_empty() {
let mut tags = if route_name.is_empty() {
vec!["default".to_string()]
} else {
vec![route_name]
};

// Add custom tag from docs directive if present
if let Some(docs) = &route.annotation.docs {
if let Some(tag) = &docs.tag {
tags = vec![tag.clone()];
}
}

let mut parameters = Self::create_path_parameters(&path_spec.params);
for query_field in &endpoint.query {
parameters.push(Self::create_query_parameter(query_field));
Expand All @@ -185,6 +196,7 @@ impl OpenAPIGenerator {
parameters,
request_body,
responses: Some(Self::create_default_responses()),
deprecated: route.annotation.docs.as_ref().map(|d| d.deprecated),
// security: Self::get_security_requirement(endpoint.annotation),
..Default::default()
}
Expand Down
1 change: 1 addition & 0 deletions examples/routes/ai.ai
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@docs(tag="Guess", deprecated=true)
get /guess {

query {
Expand Down