use crate::route::{AppRoutes, Route};
use php_literal_parser::from_str;
use streaming_iterator::StreamingIterator;
use tree_sitter::{Language, Parser, Query, QueryCursor};
pub fn extract_attribute_routes(php: &str) -> AppRoutes {
let mut parser = Parser::new();
let language: Language = tree_sitter_php::LANGUAGE_PHP.into();
parser
.set_language(&language)
.expect("Error loading PHP parser");
let tree = parser.parse(php, None).unwrap();
let attribute_query = Query::new(
&language,
r#"(attribute
(name)@name
parameters: (arguments)@arguments
)@attr"#,
)
.unwrap();
let mut argument_cursor = tree.walk();
let mut attribute_cursor = QueryCursor::new();
let mut attributes =
attribute_cursor.matches(&attribute_query, tree.root_node(), php.as_bytes());
let mut routes = AppRoutes::default();
while let Some(attribute) = attributes.next() {
let name = attribute
.nodes_for_capture_index(0)
.next()
.unwrap()
.utf8_text(php.as_bytes())
.unwrap();
let route_type = match name.split('/').next_back().unwrap() {
"FrontpageRoute" => RouteType::Frontend,
"ApiRoute" => RouteType::Ocs,
_ => {
continue;
}
};
let method = attribute
.nodes_for_capture_index(2)
.next()
.unwrap()
.parent()
.unwrap()
.parent()
.unwrap()
.parent()
.unwrap();
let method_name = method
.child_by_field_name("name")
.unwrap()
.utf8_text(php.as_bytes())
.unwrap();
let class = method.parent().unwrap().parent().unwrap();
let class_name = class
.child_by_field_name("name")
.unwrap()
.utf8_text(php.as_bytes())
.unwrap();
let mut route = Route {
name: format!(
"{}#{method_name}",
class_name.strip_suffix("Controller").unwrap_or(class_name)
),
..Route::default()
};
let arguments = attribute.nodes_for_capture_index(1).next().unwrap();
for (i, argument) in arguments
.children(&mut argument_cursor)
.filter(|node| node.kind() == "argument")
.enumerate()
{
let name = argument
.child_by_field_name("name")
.map(|name| name.utf8_text(php.as_bytes()).unwrap());
let value_str = argument
.child(argument.child_count() - 1)
.unwrap()
.utf8_text(php.as_bytes())
.unwrap();
match (name, i) {
(Some("verb"), _) | (None, 0) => {
route.verb = from_str(value_str).unwrap();
}
(Some("url"), _) | (None, 1) => {
route.url = from_str(value_str).unwrap();
}
(Some("requirements"), _) | (None, 2) => {
route.requirements = from_str(value_str).unwrap();
}
(Some("defaults"), _) | (None, 3) => {
route.defaults = from_str(value_str).unwrap();
}
(Some("root"), _) | (None, 4) => {
route.root = from_str(value_str).unwrap();
}
(Some("postfix"), _) | (None, 5) => {
route.postfix = from_str(value_str).unwrap();
}
_ => panic!("unexpected FrontpageRoute argument {name:?} at index {i}"),
}
}
match route_type {
RouteType::Frontend => routes.routes.push(route),
RouteType::Ocs => routes.ocs.push(route),
}
}
routes
}
enum RouteType {
Frontend,
Ocs,
}
#[test]
fn test_parse_attribute_routes() {
use crate::route::{Route, Verb};
use maplit::hashmap;
let expected = AppRoutes {
routes: vec![
Route {
root: None,
url: "login/webauthn/start".into(),
name: "WebAuthn#startAuthentication".into(),
verb: Verb::Post,
requirements: hashmap! {},
postfix: None,
defaults: hashmap! {},
},
Route {
root: None,
url: "login/webauthn/finish".into(),
name: "WebAuthn#finishAuthentication".into(),
verb: Verb::Post,
requirements: hashmap! {},
postfix: None,
defaults: hashmap! {},
},
],
ocs: vec![],
};
let code = include_str!("../tests/data/webauthncontroller.php");
assert_eq!(expected, extract_attribute_routes(code));
}
#[test]
fn test_parse_requirements() {
use maplit::hashmap;
use php_literal_parser::Value;
use std::collections::HashMap;
assert_eq!(
hashmap! {"version".into() => 1.into()},
from_str::<HashMap<String, Value>>("['version' => 1]").unwrap()
);
}