mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss/grind] deferred session-0007 (20260521T201327Z-3848)
This commit is contained in:
parent
d47aec54bf
commit
cf00cff5a6
6 changed files with 469 additions and 7 deletions
|
|
@ -19,8 +19,8 @@ use crate::symbol::Lang;
|
|||
use tree_sitter::Node;
|
||||
|
||||
use super::js_routes::{
|
||||
bind_path_params, find_function_params, find_route_registration, function_formal_names,
|
||||
source_imports_express,
|
||||
bind_path_params, extract_route_middleware, find_function_params, find_route_registration,
|
||||
function_formal_names, source_imports_express,
|
||||
};
|
||||
|
||||
pub struct JsExpressAdapter;
|
||||
|
|
@ -61,13 +61,14 @@ impl FrameworkAdapter for JsExpressAdapter {
|
|||
.map(|p| function_formal_names(p, file_bytes))
|
||||
.unwrap_or_default();
|
||||
let request_params = bind_path_params(&formals, &path);
|
||||
let middleware = extract_route_middleware(ast, file_bytes, &summary.name, &recv);
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
middleware,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -145,7 +146,40 @@ mod tests {
|
|||
let binding = JsExpressAdapter
|
||||
.detect(&summary("handler"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
assert_eq!(binding.route.unwrap().method, HttpMethod::DELETE);
|
||||
assert_eq!(binding.route.as_ref().unwrap().method, HttpMethod::DELETE);
|
||||
let names: Vec<_> = binding.middleware.iter().map(|m| m.name.as_str()).collect();
|
||||
assert_eq!(names, vec!["authz"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn records_chained_middleware_and_global_app_use() {
|
||||
let src: &[u8] = b"const express = require('express');\n\
|
||||
const app = express();\n\
|
||||
app.use(helmet());\n\
|
||||
app.use(logger);\n\
|
||||
function authz(req, res, next) { next(); }\n\
|
||||
function validate(req, res, next) { next(); }\n\
|
||||
function handler(req, res) { res.send('ok'); }\n\
|
||||
app.post('/save', authz, validate, handler);\n";
|
||||
let tree = parse_js(src);
|
||||
let binding = JsExpressAdapter
|
||||
.detect(&summary("handler"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
let names: Vec<_> = binding.middleware.iter().map(|m| m.name.as_str()).collect();
|
||||
assert_eq!(names, vec!["helmet", "logger", "authz", "validate"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middleware_empty_when_route_has_no_chain() {
|
||||
let src: &[u8] = b"const express = require('express');\n\
|
||||
const app = express();\n\
|
||||
function handler(req, res) { res.send('ok'); }\n\
|
||||
app.get('/x', handler);\n";
|
||||
let tree = parse_js(src);
|
||||
let binding = JsExpressAdapter
|
||||
.detect(&summary("handler"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
assert!(binding.middleware.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
//! on whether a placeholder of the same name appears in the path
|
||||
//! template.
|
||||
|
||||
use crate::dynamic::framework::{HttpMethod, ParamBinding, ParamSource};
|
||||
use crate::dynamic::framework::{HttpMethod, MiddlewareShape, ParamBinding, ParamSource};
|
||||
use tree_sitter::Node;
|
||||
|
||||
/// True when `bytes` carries any of the well-known Express import
|
||||
|
|
@ -486,6 +486,155 @@ pub fn first_string_arg(args: Node<'_>, bytes: &[u8]) -> Option<String> {
|
|||
None
|
||||
}
|
||||
|
||||
/// Walk `root` collecting middleware names attached to a route
|
||||
/// registration. Two sites are inspected:
|
||||
///
|
||||
/// 1. The positional `<receiver>.<verb>(<path>, mw1, mw2, …, handler)`
|
||||
/// chain on the matching route call — every identifier-shaped
|
||||
/// positional argument between the path string and `target`
|
||||
/// becomes a [`MiddlewareShape`].
|
||||
/// 2. Every preceding `<receiver>.use(<mw>)` call at the top level —
|
||||
/// `<mw>` may be a bare identifier (`app.use(authMw)`) or a
|
||||
/// call expression (`app.use(authMw())`), and the recorded name
|
||||
/// is the identifier / called-function last segment.
|
||||
///
|
||||
/// Names are recorded in source order: global `app.use(...)` first
|
||||
/// (because they fire before the route), then per-route chained
|
||||
/// middleware. Duplicate names are kept — repeated registrations are
|
||||
/// real, e.g. `app.use(logger); app.use(logger);`.
|
||||
pub fn extract_route_middleware(
|
||||
root: Node<'_>,
|
||||
bytes: &[u8],
|
||||
target: &str,
|
||||
receiver_accepts: &dyn Fn(&str) -> bool,
|
||||
) -> Vec<MiddlewareShape> {
|
||||
let mut global: Vec<MiddlewareShape> = Vec::new();
|
||||
let mut route_chain: Vec<MiddlewareShape> = Vec::new();
|
||||
walk_for_middleware(
|
||||
root,
|
||||
bytes,
|
||||
target,
|
||||
receiver_accepts,
|
||||
&mut global,
|
||||
&mut route_chain,
|
||||
);
|
||||
global.extend(route_chain);
|
||||
global
|
||||
}
|
||||
|
||||
fn walk_for_middleware<'a>(
|
||||
node: Node<'a>,
|
||||
bytes: &[u8],
|
||||
target: &str,
|
||||
receiver_accepts: &dyn Fn(&str) -> bool,
|
||||
global: &mut Vec<MiddlewareShape>,
|
||||
route_chain: &mut Vec<MiddlewareShape>,
|
||||
) {
|
||||
if node.kind() == "call_expression"
|
||||
&& let Some(callee) = node.child_by_field_name("function")
|
||||
&& callee.kind() == "member_expression"
|
||||
&& let Some(object) = callee.child_by_field_name("object")
|
||||
&& let Some(property) = callee.child_by_field_name("property")
|
||||
&& let Some(object_text) = object.utf8_text(bytes).ok()
|
||||
&& let Some(prop_text) = property.utf8_text(bytes).ok()
|
||||
&& receiver_accepts(last_segment(object_text))
|
||||
&& let Some(args) = node.child_by_field_name("arguments")
|
||||
{
|
||||
if prop_text == "use" {
|
||||
for name in collect_use_arg_names(args, bytes) {
|
||||
global.push(MiddlewareShape { name });
|
||||
}
|
||||
} else if http_verb_from_method(prop_text).is_some()
|
||||
&& call_args_reference_target(args, bytes, target)
|
||||
{
|
||||
for name in collect_chain_middleware_names(args, bytes, target) {
|
||||
route_chain.push(MiddlewareShape { name });
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut cur = node.walk();
|
||||
for child in node.children(&mut cur) {
|
||||
walk_for_middleware(child, bytes, target, receiver_accepts, global, route_chain);
|
||||
}
|
||||
}
|
||||
|
||||
/// Pull middleware names from a positional `(<path>, mw1, mw2, …,
|
||||
/// handler)` arguments node. Skips the leading string-literal path,
|
||||
/// stops at the named handler reference, and ignores object-literal
|
||||
/// option arguments (Fastify's `{ schema, preHandler, … }` shape is
|
||||
/// handled separately by [`collect_options_middleware_names`]).
|
||||
fn collect_chain_middleware_names(args: Node<'_>, bytes: &[u8], target: &str) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
let mut seen_path_literal = false;
|
||||
let mut cur = args.walk();
|
||||
for c in args.named_children(&mut cur) {
|
||||
match c.kind() {
|
||||
"string" | "template_string" if !seen_path_literal => {
|
||||
seen_path_literal = true;
|
||||
}
|
||||
"identifier" => {
|
||||
if let Ok(text) = c.utf8_text(bytes) {
|
||||
if text == target {
|
||||
break;
|
||||
}
|
||||
out.push(text.to_owned());
|
||||
}
|
||||
}
|
||||
"member_expression" => {
|
||||
if let Ok(text) = c.utf8_text(bytes) {
|
||||
let last = last_segment(text);
|
||||
if last == target {
|
||||
break;
|
||||
}
|
||||
out.push(last.to_owned());
|
||||
}
|
||||
}
|
||||
"call_expression" => {
|
||||
// Inline middleware factory call like `auth({ role: 'admin' })`.
|
||||
if let Some(fn_node) = c.child_by_field_name("function")
|
||||
&& let Ok(text) = fn_node.utf8_text(bytes)
|
||||
{
|
||||
out.push(last_segment(text).to_owned());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Pull middleware names from a `<receiver>.use(<mw>, [<mw>, …])` call.
|
||||
/// Each positional argument that resolves to an identifier or a call
|
||||
/// expression contributes one entry; string-named middleware modules
|
||||
/// (`app.use('/admin', adminRouter)`) skip the path string.
|
||||
fn collect_use_arg_names(args: Node<'_>, bytes: &[u8]) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
let mut cur = args.walk();
|
||||
for c in args.named_children(&mut cur) {
|
||||
match c.kind() {
|
||||
"identifier" => {
|
||||
if let Ok(text) = c.utf8_text(bytes) {
|
||||
out.push(text.to_owned());
|
||||
}
|
||||
}
|
||||
"member_expression" => {
|
||||
if let Ok(text) = c.utf8_text(bytes) {
|
||||
out.push(last_segment(text).to_owned());
|
||||
}
|
||||
}
|
||||
"call_expression" => {
|
||||
if let Some(fn_node) = c.child_by_field_name("function")
|
||||
&& let Ok(text) = fn_node.utf8_text(bytes)
|
||||
{
|
||||
out.push(last_segment(text).to_owned());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Parse a Fastify options-object call `fastify.route({ method, url,
|
||||
/// handler })` returning the bound `(method, url)` when the
|
||||
/// `handler:` property references `target`.
|
||||
|
|
@ -629,6 +778,48 @@ mod tests {
|
|||
assert_eq!(path, "/save");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_middleware_picks_up_chain_args() {
|
||||
let src: &[u8] = b"app.post('/save', authz, validate, handler);\n";
|
||||
let tree = parse_js(src);
|
||||
let recv = |n: &str| n == "app";
|
||||
let mw = extract_route_middleware(tree.root_node(), src, "handler", &recv);
|
||||
let names: Vec<_> = mw.iter().map(|m| m.name.as_str()).collect();
|
||||
assert_eq!(names, vec!["authz", "validate"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_middleware_records_app_use_in_order() {
|
||||
let src: &[u8] = b"app.use(helmet());\napp.use(logger);\napp.get('/x', handler);\n";
|
||||
let tree = parse_js(src);
|
||||
let recv = |n: &str| n == "app";
|
||||
let mw = extract_route_middleware(tree.root_node(), src, "handler", &recv);
|
||||
let names: Vec<_> = mw.iter().map(|m| m.name.as_str()).collect();
|
||||
assert_eq!(names, vec!["helmet", "logger"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_middleware_returns_empty_on_no_chain() {
|
||||
let src: &[u8] = b"app.get('/x', handler);\n";
|
||||
let tree = parse_js(src);
|
||||
let recv = |n: &str| n == "app";
|
||||
let mw = extract_route_middleware(tree.root_node(), src, "handler", &recv);
|
||||
assert!(mw.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_middleware_skips_member_expression_path_alias() {
|
||||
let src: &[u8] =
|
||||
b"app.post('/save', mw.csrf, mw.auth, controller.save);\n";
|
||||
let tree = parse_js(src);
|
||||
let recv = |n: &str| n == "app";
|
||||
let mw = extract_route_middleware(tree.root_node(), src, "save", &recv);
|
||||
let names: Vec<_> = mw.iter().map(|m| m.name.as_str()).collect();
|
||||
// `controller.save` is the handler; everything before is middleware.
|
||||
// We record the last segment of each member expression.
|
||||
assert_eq!(names, vec!["csrf", "auth"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_route_registration_matches_fastify_options_object() {
|
||||
let src: &[u8] =
|
||||
|
|
|
|||
233
src/dynamic/framework/adapters/migration_flyway.rs
Normal file
233
src/dynamic/framework/adapters/migration_flyway.rs
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
//! Flyway migration adapter (Java).
|
||||
//!
|
||||
//! Fires when the surrounding source declares a Java class extending
|
||||
//! `BaseJavaMigration` or implementing `JavaMigration` from the
|
||||
//! `org.flywaydb.core.api.migration` package, and the function under
|
||||
//! analysis is the canonical `migrate(Context)` entry point or runs
|
||||
//! JDBC DDL through the context-supplied connection.
|
||||
//!
|
||||
//! Notably does NOT fire just because a helper method is named
|
||||
//! `migrate` in a file that has no Flyway import marker. The
|
||||
//! source-shape needle plus the entry-name / DDL-callee gate together
|
||||
//! mirror the Phase 21 binding-stealing audit applied to
|
||||
//! `migration_rails` and `migration_django`.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct MigrationFlywayAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "migration-flyway";
|
||||
|
||||
fn callee_is_flyway_ddl(name: &str) -> bool {
|
||||
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
|
||||
matches!(
|
||||
last,
|
||||
"execute"
|
||||
| "executeUpdate"
|
||||
| "executeQuery"
|
||||
| "executeLargeUpdate"
|
||||
| "prepareStatement"
|
||||
| "createStatement"
|
||||
| "addBatch"
|
||||
| "executeBatch"
|
||||
)
|
||||
}
|
||||
|
||||
fn source_has_flyway_shape(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"org.flywaydb.core.api.migration.BaseJavaMigration",
|
||||
b"org.flywaydb.core.api.migration.JavaMigration",
|
||||
b"org.flywaydb.core.api.migration.Context",
|
||||
b"extends BaseJavaMigration",
|
||||
b"implements JavaMigration",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
fn name_is_migration_entry(name: &str) -> bool {
|
||||
matches!(name, "migrate")
|
||||
}
|
||||
|
||||
/// Pull the version out of the Flyway filename convention. Real
|
||||
/// Flyway parses the version from the class name (`V1_2_3__Add_users`
|
||||
/// → `1.2.3`) using the same rule documented at
|
||||
/// <https://documentation.red-gate.com/fd/migrations-184127470.html>.
|
||||
/// We approximate by scanning the file bytes for a `class V<ver>__`
|
||||
/// declaration; if missing, return `None` so the verifier can fall
|
||||
/// back to filename-based version derivation later in the pipeline.
|
||||
fn extract_version(file_bytes: &[u8]) -> Option<String> {
|
||||
let text = std::str::from_utf8(file_bytes).unwrap_or("");
|
||||
for marker in ["class V", "public class V"] {
|
||||
if let Some(idx) = text.find(marker) {
|
||||
let after = &text[idx + marker.len()..];
|
||||
if let Some(sep) = after.find("__") {
|
||||
let raw = &after[..sep];
|
||||
let normalised: String = raw
|
||||
.chars()
|
||||
.map(|c| if c == '_' { '.' } else { c })
|
||||
.collect();
|
||||
if !normalised.is_empty()
|
||||
&& normalised
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_digit() || c == '.')
|
||||
{
|
||||
return Some(normalised);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for MigrationFlywayAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Java
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
_ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
let has_shape = source_has_flyway_shape(file_bytes);
|
||||
let name_matches = name_is_migration_entry(&summary.name);
|
||||
let body_runs_ddl = super::any_callee_matches(summary, callee_is_flyway_ddl);
|
||||
let binds = has_shape && (name_matches || body_runs_ddl);
|
||||
if !binds {
|
||||
return None;
|
||||
}
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::Migration {
|
||||
version: extract_version(file_bytes),
|
||||
},
|
||||
route: None,
|
||||
request_params: Vec::new(),
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_java(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_java::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_base_java_migration_subclass() {
|
||||
let src: &[u8] = b"import org.flywaydb.core.api.migration.BaseJavaMigration;\n\
|
||||
import org.flywaydb.core.api.migration.Context;\n\
|
||||
public class V1_2_3__Add_users extends BaseJavaMigration {\n\
|
||||
public void migrate(Context context) throws Exception { }\n\
|
||||
}\n";
|
||||
let tree = parse_java(src);
|
||||
let summary = FuncSummary {
|
||||
name: "migrate".into(),
|
||||
..Default::default()
|
||||
};
|
||||
let binding = MigrationFlywayAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.expect("flyway migration binds");
|
||||
assert_eq!(binding.adapter, "migration-flyway");
|
||||
if let EntryKind::Migration { version } = binding.kind {
|
||||
assert_eq!(version.as_deref(), Some("1.2.3"));
|
||||
} else {
|
||||
panic!("expected Migration entry kind");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_when_implementing_java_migration_interface() {
|
||||
let src: &[u8] = b"import org.flywaydb.core.api.migration.JavaMigration;\n\
|
||||
public class Boot implements JavaMigration {\n\
|
||||
public void migrate(Object ctx) { }\n\
|
||||
}\n";
|
||||
let tree = parse_java(src);
|
||||
let summary = FuncSummary {
|
||||
name: "migrate".into(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
MigrationFlywayAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_some(),
|
||||
"interface-based Flyway migration must bind",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_helper_named_migrate_without_flyway_import() {
|
||||
let src: &[u8] = b"public class Helper {\n\
|
||||
public void migrate(Object ctx) { }\n\
|
||||
}\n";
|
||||
let tree = parse_java(src);
|
||||
let summary = FuncSummary {
|
||||
name: "migrate".into(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
MigrationFlywayAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none(),
|
||||
"helper named `migrate` without Flyway import must not bind",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_unrelated_method_in_flyway_file() {
|
||||
let src: &[u8] = b"import org.flywaydb.core.api.migration.BaseJavaMigration;\n\
|
||||
public class V1__Init extends BaseJavaMigration {\n\
|
||||
public void helper() { }\n\
|
||||
public void migrate(Object ctx) { }\n\
|
||||
}\n";
|
||||
let tree = parse_java(src);
|
||||
let summary = FuncSummary {
|
||||
name: "helper".into(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
MigrationFlywayAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none(),
|
||||
"helper method that does not run DDL must not bind even inside a Flyway file",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_dotted_version_from_filename_class() {
|
||||
let src: &[u8] = b"import org.flywaydb.core.api.migration.BaseJavaMigration;\n\
|
||||
public class V2_0__Seed extends BaseJavaMigration {\n\
|
||||
public void migrate(Object ctx) { }\n\
|
||||
}\n";
|
||||
let tree = parse_java(src);
|
||||
let summary = FuncSummary {
|
||||
name: "migrate".into(),
|
||||
..Default::default()
|
||||
};
|
||||
let binding = MigrationFlywayAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.expect("binds");
|
||||
if let EntryKind::Migration { version } = binding.kind {
|
||||
assert_eq!(version.as_deref(), Some("2.0"));
|
||||
} else {
|
||||
panic!("expected Migration entry kind");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -53,6 +53,7 @@ pub mod middleware_rails;
|
|||
pub mod middleware_spring;
|
||||
pub mod migration_django;
|
||||
pub mod migration_flask;
|
||||
pub mod migration_flyway;
|
||||
pub mod migration_laravel;
|
||||
pub mod migration_prisma;
|
||||
pub mod migration_rails;
|
||||
|
|
@ -156,6 +157,7 @@ pub use middleware_rails::MiddlewareRailsAdapter;
|
|||
pub use middleware_spring::MiddlewareSpringAdapter;
|
||||
pub use migration_django::MigrationDjangoAdapter;
|
||||
pub use migration_flask::MigrationFlaskAdapter;
|
||||
pub use migration_flyway::MigrationFlywayAdapter;
|
||||
pub use migration_laravel::MigrationLaravelAdapter;
|
||||
pub use migration_prisma::MigrationPrismaAdapter;
|
||||
pub use migration_rails::MigrationRailsAdapter;
|
||||
|
|
|
|||
|
|
@ -220,6 +220,7 @@ mod tests {
|
|||
// / `Middleware` / `Migration` — distributed across the
|
||||
// language slices. Per-lang deltas vs the Phase 20 baseline:
|
||||
// Java: +2 (ScheduledQuartz, MiddlewareSpring) 14 → 16
|
||||
// +1 follow-up (MigrationFlyway) 16 → 17
|
||||
// Php: +2 (MiddlewareLaravel, MigrationLaravel) 10 → 12
|
||||
// Python: +7 (GraphqlGraphene, MiddlewareDjango,
|
||||
// MigrationDjango, MigrationFlask,
|
||||
|
|
@ -237,8 +238,8 @@ mod tests {
|
|||
let java_registered = registry::adapters_for(Lang::Java);
|
||||
assert_eq!(
|
||||
java_registered.len(),
|
||||
16,
|
||||
"Java must have Phase 20 baseline (14) + M.3 Quartz/Spring-middleware (2)",
|
||||
17,
|
||||
"Java must have Phase 20 baseline (14) + M.3 Quartz/Spring-middleware (2) + Flyway (1)",
|
||||
);
|
||||
for adapter in java_registered {
|
||||
assert_eq!(adapter.lang(), Lang::Java);
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ static JAVA: &[&dyn FrameworkAdapter] = &[
|
|||
&super::adapters::KafkaJavaAdapter,
|
||||
&super::adapters::LdapSpringAdapter,
|
||||
&super::adapters::MiddlewareSpringAdapter,
|
||||
&super::adapters::MigrationFlywayAdapter,
|
||||
&super::adapters::RabbitJavaAdapter,
|
||||
&super::adapters::RedirectJavaAdapter,
|
||||
&super::adapters::ScheduledQuartzAdapter,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue